In this lesson we'll cover:
ListView
GestureDetector
Navigator
, MaterialPageRoute
and RouteFactory
You can check out the step/step06
branch here: which will contain the code for this lesson.
In this lesson, we'll be showing a list of locations, so we'll need to add more to our list of fixture data.
location.dart
file.assets/images/*.jpg
.id
field has now been added so that we can load a LocationDetail
screen by ID.// 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', [
// ...
]),
// ...
StatelessWidget
that returns a Scaffold
and a ListView
inside.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.
GestureDetector
for each ListView
ItemGestureDetector
widget, which lets us tap on a child widget.Container
as a child of each GestureDetector
presenting the location name.onTap
parameter for GestureDetector
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');
}
}
LocationDetail
to Take a Location ID Parameter// 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);
// ...
StatelessWidget
, just like we have in previous lessons.// 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.push()
method is a simple way to present our screen.MaterialPageRoute
is the required widget to use when we're using the Material Design (via import 'package:flutter/material.dart';
).MaterialPageRoute
wants us to "build" (i.e. implement) our own function which should return the widget we want to display.app.dart
.MaterialApp
widget offer a routes
parameter, allows us to associate a "name" with the instantion of the actual screen we want to present. More on this here.
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
FirstScreen
above takes an argument, it won't be possible.MaterialApp
's onGenerateRoute
parameter and a RouteFactory
:// 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);
};
}
RouteFactory
to onGenerateRoute
.RouteFactory
is defined by a Dart closure (an anonymous function), taking a RouteSettings
param defined here by the settings
variableRouteSettings
has a member called arguments
which is a basic Dart Object
name
which represents the name of the route._onLocationTap
method in locations.dart
.Navigator.pushNamed()
method and we can pass a simple Map
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,
));
}
}
AppBar
in our LocationDetail
screen, a back button automatically appears for us.Navigator.pop(context);
but this is not necessary in our case.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
.****