What You Need to Build the UI: Navigation and the InheritedWidget

Before we actually build the UI for this app, we’re going to discuss two important things: how to navigate between multiple views in Flutter and how to use InheritedWidgets. This will be a general introduction to the options Flutter offers for navigation; we’ll only use some of these in the app. If you’d prefer to jump ahead to more advanced navigation, take a look at Build the App’s Basic UI and then come back here when you want to learn about how to implement navigation in Flutter apps.

Navigation in Flutter

Flutter offers multiple options for navigating between views:

  • push/pop navigation, useful when there is a home page and many pages accessible from it, potentially using a Drawer menu.

  • Page-by-page navigation using the PageView and PageController, useful when there are a limited number of pages that can be considered on the same level.

Push/Pop Navigation Using the Navigator

Making an app that takes advantage of the Navigator for push/pop navigation is really simple.

Define the Pages

The first thing to do is to define the pages, which will be really simple:

 class​ NewPage ​extends​ StatelessWidget {
 
  @override
  Widget build(BuildContext context) {
 return​ Scaffold(
  appBar: AppBar(title: Text(​"New Page"​),),
  body: Center(
  child: FlatButton(
  color: Colors.black12,
  onPressed: () {},
  child: Text(​"Go back to the home page"​),
  )
  ),
  );
  }
 }
 
 
 class​ HomePage ​extends​ StatelessWidget {
 
  @override
  Widget build(BuildContext context) {
 return​ Scaffold(
  appBar: AppBar(title: Text(​"Home Page"​),),
  body: Center(
  child: Container(
  height: 100,
  child: Card(
  child: Column(
  children: <Widget>[
  Padding(
  padding: EdgeInsets.all(10.0),
  child: Text(
 "Click the button to go to a new page"​,
  style: Theme.of(context).textTheme.title,
  ),
  ),
  ButtonTheme.bar(
  child: FlatButton(
  child: Text(​"Go to new page"​),
  onPressed: () {}
  ),
  )
  ],
  )
  ),
  ),
  ),
  );
  }
 }

which generates the following app in its initial state as shown in the screenshot.

images/WidgetLayout/NavigationHomePage.png

I made the home page a bit more complex than it needed to be to differentiate the two pages visually.

Now comes the important part: that onPressed option for both pages, inside which we are going to add the navigation code.

I called this type of navigation &lquot;push&rquot;/&lquot;pop&rquot; because it uses two methods called Navigator.push and Navigator.pop.

Push

You can navigate to a new page (contained in a Widget called NewPage) using the Navigator.push in the following way:

 Navigator.push(
  context,
  MaterialPageRoute(
  builder: (context) => NewPage(),
  ),
 );

Navigator.push takes two arguments: the BuildContext and a Route to the new page.

You can create a route using the MaterialPageRoute constructor, which uses a builder callback function (which can be a call to the widget’s constructor, as is the case in the example) to build the new view.

You can put that code inside the curly braces of the onPressed option for the FlatButton inside MyHomePage and the following view will be displayed when the user taps the button:

images/WidgetLayout/NavigationNewPage.png

Pop

Even though the framework by itself has added a (working, by the way) back button in the top left, it is useful to know how to make a custom button to go back.

It is also incredibly simple:

 Navigator.pop(context);

Adding that code to the curly braces of the onPressed attribute of NewPage’s FlatButton, tapping that button in the middle of the screen will make the app go back to the home page.

Using a Drawer Side Navigation Menu

A commonly seen navigation element is the Drawer,[39] which is the menu, seen in many apps, that opens by swiping right starting from the left edge of the screen or by tapping the commonly seen button with three vertically stacked horizontal lines.

Like many items that are commonly used and placed outside the app body, a drawer is added to the app by using the drawer option of the Scaffold and setting it to a Drawer object:

 Scaffold(
  appBar: AppBar(title: ...),
  drawer: Drawer(
  child: ...
  ),
  body: ...,
 )

The Drawer class takes just three arguments (excluding the Key), which makes it one of the simplest widgets in the entire Flutter standard library in this respect.

Since one of them is the semanticLabel (which is a string used by accessibility tools such as screen readers to describe what they’re seeing) and the other is the elevation (which is a double, also present in the Card, which is used to customize the shadow behind it), the only one that we’re currently interested in is the child, which is usually a Column or a ListView (if you’re worried about items not having enough vertical space).

For this example, we’ll use a ListView.

Very often, the first element in the list is a DrawerHeader, which is a lot like a Container and usually includes either account information and facilities to switch accounts (if present), the name of the app and/or information about its creators, or a short description of what the menu is for.

Below it, a number of ListTiles are used to display navigation options.

If you decide to use a ListView, you should set its padding option to EdgeInsets.zero to avoid having gaps at the top and bottom of the screen.

Since we’re talking about navigation so much, a Drawer for a public transportation or travel app could be the following:

 Drawer(
  child: ListView(
  padding: EdgeInsets.zero,
  children: [
  DrawerHeader(
  decoration: BoxDecoration(
  color: Colors.red,
  ),
  child: Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  mainAxisAlignment: MainAxisAlignment.end,
  children: [
  Text(
 "Profile Name"​,
  style: TextStyle(
  color: Colors.white,
  fontSize: 18,
  ),
  ),
  Text(
 "[email protected]"​,
  style: TextStyle(
  color: Colors.white,
  fontSize: 11,
  fontWeight: FontWeight.w300
  ),
  ),
  ]
  ),
  ),
  ListTile(
  leading: Icon(Icons.train),
  title: Text(
 "Tickets"​,
  style: Theme.of(context).textTheme.title,
  ),
  ),
  ListTile(title: Text(​"Buy Tickets"​), onTap: () {}),
  ListTile(title: Text(​"My Tickets"​), onTap: () {}),
  Divider(),
  ListTile(
  leading: Icon(Icons.person),
  title: Text(
 "Profile"​,
  style: Theme.of(context).textTheme.title,
  ),
  ),
  ListTile(title: Text(​"Profile information"​), onTap: () {}),
  ListTile(title: Text(​"Past trips"​), onTap: () {}),
  ListTile(title: Text(​"Loyalty program points"​), onTap: () {}),
  ],
  )
 ),

First of all, it’s important to clarify that just adding a drawer to a Scaffold causes the framework to automatically add a button to the app bar to expand it, as you can see with an empty Scaffold body as shown in the screenshot.

images/WidgetLayout/travelhome.png

The Drawer itself looks like this:

images/WidgetLayout/traveldrawer.png

At this point, you might not be sure about how to actually use the drawer for navigation purposes, and that is because it’s not done using Navigator.push, but by changing the state of the home page and some data that can be used by build to build the page and, after doing that, calling Navigator.pop to go back to the home page.

This means the home page has to be a stateful widget (don’t forget that, especially if you get a setState isn’t defined for HomePage error) and it is a great chance to introduce Dart enums.

Enumerated Types

You can create an enumerated type of data by code like the following:

 enum​ PageType {
  buyTickets,
  myTickets,
  profileInfo,
  pastTrips,
  myPoints
 }

which creates a new type called PageType which can only be one of PageType.buyTickets, PageType.myTickets, PageType.profileInfo, PageType.pastTrips, or PageInfo.myPoints.

Even if you are not used to this kind of data type, you’ll surely be able to understand it with the continuation of the travel app example.

Finishing Our Travel App’s Navigation

To finish the travel app’s navigation, add the ​code​​​ to your app outside any class definition and a complete, working example of a State that implements the functionality I described is the following:

 class​ HomePageState ​extends​ State<HomePage> {
 
  PageType _pageType = PageType.buyTickets;
 String​ _str;
 String​ _sub;
 
  @override
  Widget build(BuildContext context) {
 switch​(_pageType) {
 case​ PageType.buyTickets:
  _str = ​"Buy Tickets"​;
  _sub = ​"You can buy your tickets here"​;
 break​;
 case​ PageType.myPoints:
  _str = ​"My Points"​;
  _sub = ​"You can buy check your loyalty program points here"​;
 break​;
 case​ PageType.myTickets:
  _str = ​"My Tickets"​;
  _sub = ​"You can see the tickets you've bought here"​;
 break​;
 case​ PageType.pastTrips:
  _str = ​"My Past Trips"​;
  _sub = ​"You can check the trips you have made previously here"​;
 break​;
 case​ PageType.profileInfo:
  _str = ​"My Profile Information"​;
  _sub = ​"You can see your profile information here"​;
  }
 return​ Scaffold(
  appBar: AppBar(
  title: Text(​"Programming Travels"​),
  backgroundColor: Colors.red,
  ),
  drawer: Drawer(
  child: ListView(
  padding: EdgeInsets.zero,
  children: [
  DrawerHeader(
  decoration: BoxDecoration(
  color: Colors.red,
  ),
  child: Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  mainAxisAlignment: MainAxisAlignment.end,
  children: [
  Text(
 "Profile Name"​,
  style: TextStyle(
  color: Colors.white,
  fontSize: 18,
  ),
  ),
  Text(
 "[email protected]"​,
  style: TextStyle(
  color: Colors.white,
  fontSize: 11,
  fontWeight: FontWeight.w300
  ),
  ),
  ]
  ),
  ),
  ListTile(
  leading: Icon(Icons.train),
  title: Text(
 "Tickets"​,
  style: Theme.of(context).textTheme.title,
  ),
 ),
 ListTile(
  title: Text(​"Buy Tickets"​),
  onTap: () {
  setState(
  () => _pageType = PageType.buyTickets,
  );
  Navigator.pop(context);
  }
 ),
 ListTile(
  title: Text(​"My Tickets"​),
  onTap: () {
  setState(
  () => _pageType = PageType.myTickets,
  );
  Navigator.pop(context);
  }
 ),
 Divider(),
 ListTile(
  leading: Icon(Icons.person),
  title: Text(
 "Profile"​,
  style: Theme.of(context).textTheme.title,
  ),
 ),
 ListTile(
  title: Text(​"Profile Information"​),
  onTap: () {
  setState(
  () => _pageType = PageType.profileInfo,
  );
  Navigator.pop(context);
  }
 ),
 ListTile(
  title: Text(​"Past Trips"​),
  onTap: () {
  setState(
  () => _pageType = PageType.pastTrips,
  );
  Navigator.pop(context);
  }
 ),
  ListTile(
  title: Text(​"Loyalty Program Points"​),
  onTap: () {
  setState(
  () => _pageType = PageType.myPoints,
  );
  Navigator.pop(context);
  }
  ),
  ],
  )
  ),
  Center(
  child: Padding(
  padding: EdgeInsets.all(10.0),
  child: Column(
  children: [
  Text(_str, style: Theme.of(context).textTheme.title),
  Text(_sub, style: Theme.of(context).textTheme.subtitle)
  ],
  ),
  ),
  ),
  );
  }
 }

As you can see, each of the Drawer’s ListTile sets the _pageType variable (which, by default, is PageType.buyTickets), reloads the home page using setState and then calls Navigator.pop to close the drawer.

build checks which of them is the current _pageType and sets two strings (a title and a subtitle) accordingly. Those two strings are then displayed in a centered and padded Column as shown in the screenshot.

images/WidgetLayout/ProgrammingTravel.png

The result is working navigation using a Drawer menu.

Page-by-Page Navigation

Swipeable page-by-page navigation can be achieved using two kinds of view: the PageView and the TabBarView, the difference is that the first only allows navigation by swiping, whereas the second is used together with a TabBar below the AppBar.

If, instead, you just want something like the YouTube app: being able to switch between pages using a menu at the bottom of the screen, the way to go is with a BottomNavigationBar.

BottomNavigationBar

A BottomNavigationBar works a lot like a Drawer, but it’s always visible at the bottom, so you don’t need to use Navigator.pop.

More specifically, you add it to the Scaffold’s bottomNavigationBar option, and you use it like this:

 return​ ​Scaffold​(
 // app body, appbar, etc.
  bottomNavigationBar: BottomNavigationBar(
  currentIndex: index,
  items: [
  BottomNavigationBarItem(
  icon: Icon( ... ),
  title: Text( ... ),
  ),
  BottomNavigationBarItem(
  icon: Icon( ... ),
  title: Text( ... ),
  ),
 // potentially more navigation items...
  ],
  onTap: (page) {
  setState(() {
 switch​(page) {
 case​ 0:
 // Things to do if the user selects the first item
  index = 0;
 break​;
 case​ 1:
 // Things to do if the user selects the second item
  index = 1;
 // Potentially other cases for more navigation items
  }
  });
  },
  ),
 )

It has three main options:

  • The items, which are the navigation options to be given to the user.

  • The currentIndex, the value to which you assign will have to be changed manually when the user taps on a navigation item.

  • The onTap callback, which takes an integer indicating which option has been tapped by the user.

A practical example of a full app home page class could be the following:

 String​ _str;
 int​ index = 0;
 
 @override
 Widget ​build​(BuildContext context) {
 switch​(index) {
 case​ 0:
  _str = ​"Home"​;
 break​;
 case​ 1:
  _str = ​"Page 2"​;
 break​;
 case​ 2:
  _str = ​"Page 3"​;
  }
 return​ Scaffold(
  appBar: AppBar(
  title: Text(​"Bottom Navigation Bar Example"​),
  backgroundColor: Colors.red,
  ),
  body: Center(
  child: Text(
 "​​$_str​​"​,
  style: Theme.of(context).textTheme.display1
  ),
  ),
  bottomNavigationBar: BottomNavigationBar(
  currentIndex: index,
  items: [
  BottomNavigationBarItem(
  icon: Icon(Icons.home),
  title: Text(​"Home"​),
  ),
  BottomNavigationBarItem(
  icon: Icon(Icons.looks_two),
  title: Text(​"Page 2"​),
  ),
  BottomNavigationBarItem(
  icon: Icon(Icons.format_list_numbered_rtl),
  title: Text(​"Page 3"​),
  ),
  ],
  onTap: (page) {
  setState(() {
 switch​(page) {
 case​ 0:
  index = 0;
 break​;
 case​ 1:
  index = 1;
 break​;
 case​ 2:
  index = 2;
  }
  });
  },
  ),
  );
 }

which looks like the screenshot and produces the desired behavior when the user taps on a navigation option.

images/WidgetLayout/BottomNavigation.png

PageView

The PageView is comparable to a horizontal ListView, but that only scrolls page by page.

You can use it as a part of the overall layout, but also as the entire body of the app’s Scaffold, like in the following example:

 Scaffold(
  appBar: AppBar(
  title: Text(​"PageView Example"​),
  backgroundColor: Colors.teal,
  ),
  body: PageView(
  children: <Widget>[
  Center(
  child: Text(
 "Swipe left"​,
  style: Theme.of(context).textTheme.display1
  ),
  ),
  Center(
  child: Padding(
  padding: EdgeInsets.all(10.0),
  child: Text(
 "Swipe left again, or swipe right"​,
  style: Theme.of(context).textTheme.display1
  ),
  ),
  ),
  Center(
  child: Text(
 "Swipe right"​,
  style: Theme.of(context).textTheme.display1
  ),
  ),
  ],
  ),
 )

which produces a simple view with an AppBar and, in the middle of the screen, a Text widget.

There is no other on-screen indication that the view has other pages, but swiping left moves the view to the next page and swiping right moves the view to the previous page.

In addition to the children, the PageView has a controller attribute, which can be set to a PageController.

The PageController has a useful option: the initialPage, which is an integer specifying the page (starting from 0) at which the view starts when the app loads.

Top Navigation with a TabBar and TabBarView

Significantly more useful and common is the TabBarView which, paired with a TabBar below the AppBar, provides the same swiping experience of a PageView combined with a nice menu at the top to select which page to switch to and to highlight the page the user is currently at.

The TabBarView and the TabBar are linked by a TabController.

An elegant way to create one is by wrapping the entire view in a DefaultTabController, and setting the entire Scaffold as its child, the number of tabs as its length, and (optionally) an initialIndex (starting from 0, which is the default).

The TabBar can be set as the AppBar’s bottom option and it has a tab option where you’ll add the Tab widgets, made up of a text and an optional icon, which will be the menu items which will make up the TabBar.

The TabBarView will instead be set as the Scaffold’s body and you will add the pages to be displayed to its children option.

For example, this:

 DefaultTabController(
  length: 3,
 // OPTIONAL initialIndex: 0,
  child: Scaffold(
  appBar: AppBar(
  title: Text(​"TabBarView Example"​),
  bottom: TabBar(
  tabs: <Widget>[
  Tab(
  icon: Icon(Icons.looks_one),
  text: ​"First Page"​,
  ),
  Tab(
  icon: Icon(Icons.looks_two),
  text: ​"Second Page"​,
  ),
  Tab(
  icon: Icon(Icons.looks_3),
  text: ​"Third Page"​,
  ),
  ],
  ),
  ),
  body: TabBarView(
  children: <Widget>[
  Center(
  child: Text(​"This is the first page"​)
  ),
  Center(
  child: Text(​"This is the second page"​)
  ),
  Center(
  child: Text(​"This is the third page"​)
  ),
  ],
  )
  ),
 )

which produces the result as shown in the screenshot.

images/WidgetLayout/TabBarView.png

InheritedWidgets

The DefaultTabController is a special kind of widget: an InheritedWidget, which is used to share state information between Flutter widgets, by storing them in a widget that sits at the top of the widget tree and allowing direct access to its fields and methods by its children, so that they don’t need to be passed down the tree using constructor arguments.

They are usually accessed using the Widget.of(context).fieldName syntax that we used previously to get theming data from the Theme and to show snackbars in the Scaffold. That is because both the Theme and the Scaffold are actually InheritedWidgets.

An InheritedWidget is defined in the following way:

 class​ Example ​extends​ InheritedWidget {
  Example({child, ​this​.field}):
 super​(child: child);
 final​ ​String​ field;
 
  @override
 bool​ updateShouldNotify(Example old) => ​true​;
 }

updateShouldNotify is used to determine whether the children widgets have to be rebuilt when the InheritedWidget gets rebuilt by the framework.

That will usually be true, but there you can also set it to a set of conditions to determine whether, for example, there are changes to the widget’s fields and only rebuild the children in that case.

In its current state, Example.of(context).fieldName from a child of Example wouldn’t work since there is actually no of method.

Even though it is very common, it is not actually built into the framework: by default you access parent InheritedWidgets using context.inheritFromWidgetOfExactType(Example).

In fact, a very common static method (explained on Static Members) defined inside InheritedWidgets is the of method which, in the case of an InheritedWidget called Example, can be defined in the following way:

 static​ Example ​of​(BuildContext context) =>
  context.inheritFromWidgetOfExactType(Example);

so that our entire Example class definition becomes:

 class​ Example ​extends​ InheritedWidget {
  Example({child, ​this​.field}): ​super​(child: child);
 final​ ​String​ field;
 
  @override
 bool​ updateShouldNotify(Example old) => old.field != field;
 
 static​ Example of(BuildContext context) =>
  context.inheritFromWidgetOfExactType(Example);
 }

You can use it inside an app like this:

 Example(
  field: ​"An example of a string"​,
  child: MyCustomExternalWidget(),
 )

And, at any point inside the definition of MyCustomExternalWidget, which takes no arguments, you could access that field you set using:

 Example.of(context).field

which returns the exact String that was set earlier.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset