Navigation in Flutter
Summary
In this lesson we'll cover:
- Using
ListView
- Handling tap events with
GestureDetector
- Navigation and parameterized Named Routes with
Navigator
,MaterialPageRoute
andRouteFactory
The Code for This Lesson
You can check out the step/step06
branch here: which will contain the code for this lesson.
Updating Our Fixture Data
In this lesson, we'll be showing a list of locations, so we'll need to add more to our list of fixture data.
- Check out the code cited above and copy and paste the latest
location.dart
file. - Also copy the latest three images in
assets/images/*.jpg
. - Note that an
id
field has now been added so that we can load aLocationDetail
screen by ID. - Example:
// models/location.dart
class Location {
final int id;
// ...
static List<Location> fetchAll() {
return [
Location(1, 'Kiyomizu-dera', 'assets/images/kiyomizu-dera.jpg', [
// ...
]),
Location(2, 'Mount Fuji', 'assets/images/fuji.jpg', [
// ...
]),
Location(3, 'Arashiyama Bamboo Grove', 'assets/images/arashiyama.jpg', [
// ...
]),
// ...
Implementing our Location Listing Screen
- Create a new
StatelessWidget
that returns aScaffold
and aListView
inside. - We simply fetch a list of locations and iterate over them using
map()
. ListView
will render a full screen, scrollable list of items.
// locations/locations.dart
import 'package:flutter/material.dart';
import '../../app.dart';
import '../../models/location.dart';
class Locations extends StatelessWidget {
@override
Widget build(BuildContext context) {
// NOTE: we'll be moving this to a scoped_model later
final locations = Location.fetchAll();
return Scaffold(
appBar: AppBar(
title: Text('Locations'),
),
body: ListView(
children: locations
.map((location) => Text(location.name))
.toList(),
),
);
}
// ...
}
WARNING: the above code snippet will not be our final implementation in this lesson.
Implementing a GestureDetector
for each ListView
Item
- Let's complete our implementation by adding a
GestureDetector
widget, which lets us tap on a child widget. - We'll add a
Container
as a child of eachGestureDetector
presenting the location name. - The
onTap
parameter forGestureDetector
takes a Dart closure (covered in the previous lesson). We can then invoke our_onLocationTap
function, passing in the ID of the location. _onLocationTap
will contain the implementation of our tap event (to be filled out at the end of the lesson).
// locations/locations.dart
import 'package:flutter/material.dart';
import '../../app.dart';
import '../../models/location.dart';
class Locations extends StatelessWidget {
@override
Widget build(BuildContext context) {
// NOTE: we'll be moving this to a scoped_model later
final locations = Location.fetchAll();
return Scaffold(
appBar: AppBar(
title: Text('Locations'),
),
body: ListView(
children: locations
.map((location) => GestureDetector(
onTap: () => _onLocationTap(context, location.id),
child: Container(child: Text(location.name))))
.toList(),
),
);
}
_onLocationTap(BuildContext context, int locationID) {
// TODO later in this lesson, navigation!
print('do something');
}
}
Updating our LocationDetail
to Take a Location ID Parameter
- Let's make our Location Detail screen more realistic now.
- We should be able to load this screen by passing in the unique "ID" of a location (which we set up in the first step of this lesson).
- When the Location Detail screen loads, we can then fetch the full information for the location by ID:
// location_detail/location_detail.dart
// ...
class LocationDetail extends StatelessWidget {
final int _locationID;
LocationDetail(this._locationID);
@override
Widget build(BuildContext context) {
final location = Location.fetchByID(_locationID);
// ...
- Here we simply create a new private member and constructor to parameterize our
StatelessWidget
, just like we have in previous lessons. - Here's the final implementation of this screen now for this lesson:
// location_detail/location_detail.dart
import 'package:flutter/material.dart';
import '../../models/location.dart';
import 'image_banner.dart';
import 'text_section.dart';
class LocationDetail extends StatelessWidget {
final int _locationID;
LocationDetail(this._locationID);
@override
Widget build(BuildContext context) {
// NOTE: we'll be moving this to a scoped_model later
final location = Location.fetchByID(_locationID);
return Scaffold(
appBar: AppBar(
title: Text(location.name),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ImageBanner(location.imagePath),
]..addAll(textSections(location))),
);
}
List<Widget> textSections(Location location) {
return location.facts
.map((fact) => TextSection(fact.title, fact.text))
.toList();
}
}
Navigator
, MaterialPageRoute
Now let's talk about navigation. We'll implement the bit of logic that handles the tap event we added in the previous step now.
"Navigation" is simply the ability to move from screen to screen in a mobile or web app. A navigation "route" is effectively a name we define for where our screen lives, kind of like a URL.
So for example, if we've implemented a screen implemented as widget Screen1
we can navigate to it via:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Screen1()),
);
NOTE this example was taken from
Navigator
here is something offered by Flutter.- Its
push()
method is a simple way to present our screen. MaterialPageRoute
is the required widget to use when we're using the Material Design (viaimport 'package:flutter/material.dart';
).MaterialPageRoute
wants us to "build" (i.e. implement) our own function which should return the widget we want to display.- The problem with this approach is that it's not very re-usable. If we have to present this screen in many places in our code, it's not very "DRY".
Using Named Routes
- Let's implement all of our routes in a centralized place,
app.dart
. - Our
MaterialApp
widget offer aroutes
parameter, allows us to associate a "name" with the instantion of the actual screen we want to present. More on this here. - For example:
MaterialApp(
// Start the app with the "/" named route. In our case, the app will start
// on the FirstScreen Widget
initialRoute: '/',
routes: {
// When we navigate to the "/" route, build the FirstScreen Widget
'/': (context) => FirstScreen(),
// When we navigate to the "/second" route, build the SecondScreen Widget
'/second': (context) => SecondScreen(),
},
);
We can then use the route and refer to it by name:
Navigator.pushNamed(context, '/second');
NOTE this example was taken from
Passing Arguments to Named Routes
- The problem with the above example is that we can't pass any information to the screen we'd want to load. So if say
FirstScreen
above takes an argument, it won't be possible. - The official documentation on using parameterized routes presents quite a poor solution IMO.
- It requires us to create a separate widget just to pass an argument.
- There's I think a better way to do this. We can use
MaterialApp
'sonGenerateRoute
parameter and aRouteFactory
:
// app.dart
// ...
MaterialApp(
onGenerateRoute: _routes(),
);
// ...
RouteFactory _routes() {
return (settings) {
final Map<String, dynamic> arguments = settings.arguments;
Widget screen;
switch (settings.name) {
case LocationsRoute:
screen = Locations();
break;
case LocationDetailRoute:
screen = LocationDetail(arguments['id']);
break;
default:
return null;
}
return MaterialPageRoute(builder: (BuildContext context) => screen);
};
}
- Here, we pass a
RouteFactory
toonGenerateRoute
. - A
RouteFactory
is defined by a Dart closure (an anonymous function), taking aRouteSettings
param defined here by thesettings
variable RouteSettings
has a member calledarguments
which is a basic DartObject
- It also has a member called
name
which represents the name of the route. - The rest of the function defines how a given route is built based on the route name.
Adding Our Navigation Event
- We can now update our
_onLocationTap
method inlocations.dart
. - We use the
Navigator.pushNamed()
method and we can pass a simpleMap
of arguments:
// ...
_onLocationTap(BuildContext context, int locationID) {
Navigator.pushNamed(context, LocationDetailRoute,
arguments: {"id": locationID});
}
Here's the final locations.dart
file:
// locations/locations.dart
import 'package:flutter/material.dart';
import '../../app.dart';
import '../../models/location.dart';
class Locations extends StatelessWidget {
@override
Widget build(BuildContext context) {
// NOTE: we'll be moving this to a scoped_model later
final locations = Location.fetchAll();
return Scaffold(
appBar: AppBar(
title: Text('Locations'),
),
body: ListView(
children: locations
.map((location) => GestureDetector(
onTap: () => _onLocationTap(context, location.id),
child: Container(child: Text(location.name))))
.toList(),
),
);
}
_onLocationTap(BuildContext context, int locationID) {
Navigator.pushNamed(context, LocationDetailRoute,
arguments: {"id": locationID});
}
}
Here's the final app.dart
file:
// app.dart
import 'package:flutter/material.dart';
import 'screens/locations/locations.dart';
import 'screens/location_detail/location_detail.dart';
import 'style.dart';
const LocationsRoute = '/';
const LocationDetailRoute = '/location_detail';
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateRoute: _routes(),
theme: _theme(),
);
}
RouteFactory _routes() {
return (settings) {
final Map<String, dynamic> arguments = settings.arguments;
Widget screen;
switch (settings.name) {
case LocationsRoute:
screen = Locations();
break;
case LocationDetailRoute:
screen = LocationDetail(arguments['id']);
break;
default:
return null;
}
return MaterialPageRoute(builder: (BuildContext context) => screen);
};
}
ThemeData _theme() {
return ThemeData(
appBarTheme: AppBarTheme(textTheme: TextTheme(title: AppBarTextStyle)),
textTheme: TextTheme(
title: TitleTextStyle,
body1: Body1TextStyle,
));
}
}
How to 'Go Back'
- Because we're using an
AppBar
in ourLocationDetail
screen, a back button automatically appears for us. - If we want to define the ability to go back, we can use
Navigator.pop(context);
but this is not necessary in our case.
Summary
In this lesson we covered a realistic example of navigation as well as how to pass arguments to the screen we are navigating to. We've also avoided the official documentation's not-so-ideal "Pass Arguments to a Named Route" approach by using MaterialApp
's onGenerateRoute
.****