Once the DevFest season is closed, is time to get back to the Flutter Planets tutorial. This time, we will see how to make the list a bit more alive by navigating into the detail page of each planet.

Job description

Right now we have a fancy list of planets. But this list is not doing anything, let’s see how to make the each item of the list show the appropiate detail page for each planet.

Routing and navigation

Each screen of our application is called a Route in Flutter. And the responsible to go through these screens is a Navigator. Similar to Android, the Routes are set in a navigation stack, even the two main navigation operations are push and pop

Defining Routing at application level

The easiest way to define navigation is doing it at the MaterialApp declaration. For example, if we want to declare a page for the planet detail, we will add the following to our MaterialApp object:

1new MaterialApp(
2  title: "Planets",
3  home: new HomePage(),
4  routes: <String, WidgetBuilder>{
5    '/detail': (_) => new DetailPage(),
6  },

As you can see, the routes parameter defines a dictionary of String keys. Each key is the name of the path to the page in web-ish format. Each item contain a function of type WidgetBuilder that receives the current context and returns the Widget to show as a page, in this case, a DetailPage.

There is another page declared, in this case, trough the home parameter, and its route is always “/”.

After declaring the route, we can do that our PlanetRow object navigates to this page on click. In order to do this, we should wrap our whole row widget in a GestureDetector Widget.

From this:

 1return new Container(
 2  height: 120.0,
 3  margin: const EdgeInsets.symmetric(
 4    vertical: 16.0,
 5    horizontal: 24.0,
 6  ),
 7  child: new Stack(
 8    children: <Widget>[
 9      planetCard,
10      planetThumbnail,
11    ],
12  )

To this:

 1return new GestureDetector(
 2  onTap: () => Navigator.pushNamed(context, "/detail"),
 3  child: new Container(
 4    height: 120.0,
 5    margin: const EdgeInsets.symmetric(
 6      vertical: 16.0,
 7      horizontal: 24.0,
 8    ),
 9    child: new Stack(
10      children: <Widget>[
11        planetCard,
12        planetThumbnail,
13      ],
14    ),
15  )

The GestureDetector has several parameters like onTap, onDoubleTap, onLongPress, etc, that allow us to handle all the physical interactions with the user. In this case, we only use the onTap parameter and we provide it with the function to execute in case of tap.

This function executes the method pushNamed of the class Navigator and needs two parameters, the current context and the path to navigate to, “/detail” for the example.

Now we can create a simple detail page:

 1class DetailPage extends StatelessWidget{
 2  @override
 3  Widget build(BuildContext context) {
 4    return new Scaffold(
 5      appBar: new AppBar(
 6        title: new Text("Planet Detail"),
 7      ),
 8      body: new Center(
 9        child: new RaisedButton(
10          onPressed: () => Navigator.pop(context),
11          child: new Text("<<< Go back"))
12      ),
13    );
14  }

Several things happen here. First we created an Scaffold with an AppBar, if you execute this code, you’ll see it automatically adds the back button as it detects we are in a secondary page (and it works!). Second, we created a button that executes the Navigator.pop() method, so it also gets back.

The user then has three ways to get back: via back button, via the button we put on the center of the screen, and third one is different for Android or iOS. For android the back button on the phone works, for iOS, the left swipe gesture works.

Passing parameters

Unfortunately, the routing table method doesn’t allow us to send parameters to the new screen (at least out of the box).

In order to do this, we need to generate our Route on the fly.

First, remove the routes parameter from our MaterialApp (or you can left it, but we will not use it).

And modify the GestureDetector like this:

 1return new GestureDetector(
 2  onTap: () => Navigator.of(context).push(new PageRouteBuilder(
 3    pageBuilder: (_, __, ___) => new DetailPage(planet),
 4  )),
 5  child: new Container(
 6    height: 120.0,
 7    margin: const EdgeInsets.symmetric(
 8      vertical: 16.0,
 9      horizontal: 24.0,
10    ),
11    child: new Stack(
12      children: <Widget>[
13        planetCard,
14        planetThumbnail,
15      ],
16    ),
17  )

Now, instead of recovering the PageRoute from the table of routes with pushNamed we are creating a new one using the class PageRoutBuilder, and it has a parameter named pageBuilder that should return a new Widget to show as page, in this case, DetailPage receiving as a parameter the planet to show.

Another interesting parameter from PageRouteBuilder is transitionBuilder. I dare you to play with it to modify the transitions used during navigation.

Now, we need to modify the DetailPage to show something about the planet received.

 1class DetailPage extends StatelessWidget {
 3  final Planet planet;
 5  DetailPage(this.planet);
 7  @override
 8  Widget build(BuildContext context) {
 9    return new Scaffold(
10      body: new Container(
11        constraints: new BoxConstraints.expand(),
12        child: new Column(
13          mainAxisAlignment: MainAxisAlignment.center,
14          children: <Widget>[
15            new Text(planet.name),
16            new Image.asset(planet.image, width: 96.0, height: 96.0,),
17          ],
18        ),
19      ),
20    );
21  }

A simple layout that just shows the planet name and planet image in the screen:

planet card

The cool part, adding a Hero

We will finish this article with something simple and really cool. Heroes.

Hero is a Widget intended to perform easy animations between screens. To use it, just do these two little changes.

In the PlanetRow class, modify the planetThumbnail to this:

 1final planetThumbnail = new Container(
 2  margin: new EdgeInsets.symmetric(
 3    vertical: 16.0
 4  ),
 5  alignment: FractionalOffset.centerLeft,
 6  child: new Hero(
 7      tag: "planet-hero-${planet.id}",
 8      child: new Image(
 9      image: new AssetImage(planet.image),
10      height: 92.0,
11      width: 92.0,
12    ),
13  ),

As you can see, we just wrapped the image in a Hero widget, we also added a tag, with the value “planet-hero-” plus the planet id.

And now we do the same on the planet detail page.

 1class DetailPage extends StatelessWidget {
 3  final Planet planet;
 5  DetailPage(this.planet);
 7  @override
 8  Widget build(BuildContext context) {
 9    return new Scaffold(
10      body: new Container(
11        color: const Color(0xFF736AB7),
12        constraints: new BoxConstraints.expand(),
13        child: new Column(
14          mainAxisAlignment: MainAxisAlignment.center,
15          children: <Widget>[
16            new Text(planet.name),
17            new Hero(tag: "planet-hero-${planet.id}",
18              child: new Image.asset(
19                  planet.image,
20                  width: 96.0,
21                  height: 96.0,
22              ),
23            )
24          ],
25        ),
26      ),
27    );
28  }

See? exactly the same for the image on the detail.

Now execute it, and you will see your planets flying around when moving from page to page (or back). Cool and easy, isn’t it?

The only thing to keep in mind, is that we can not have two heroes in the Widget tree with the same tag, that’s why we added the id of the planet to the tag, and, in order to animate, the tag should be the same in both pages.

To be continued

Ok, now we know a lot of things about routing:

  • How to do a simple routing using routing tables
  • How to get back once we navigate to a page
  • How to do more advancing routing creating our own Router
  • How to add a bit of pizzazz to our transitions using the super cool heroes

If you don’t want to write code, you can find this example on the branch Lesson_5_Flutter_Planets_Navigation of the repository.

For the next chapter, we will tidy up some things and start creating a georgeous detail learning some more things about layout.

Stay tuned!