Building Lists and Navigations — Flutter Tutorial Part 3

With the basic landscape detail view set up, you need to provide a way for users to see the full list of landscape, and to read the details about each of them.

In this story, you’ll create widgets that can show information about any landscape, and dynamically generate a scrolling list that a user can tap to see a detail view for a landscape.

Result

We will make a list page and a detail, and then we will link them with :)

Creating a Landscape Model

In the first part, you hard-coded information into all of your custom widgets. But in order to use these information for different cases and for convenience, a model is recommended. So here you’ll create a model to store data that you can pass into your views.

Just let’s use the completed project from the previous tutorial to get started.

Create a new file in folder. Define a structure with a few properties matching names of some of the keys that we will use in our widgets. They are and .

class LandscapeModel {
final String name;
final String country;
final String park;
final String assetPath;
LandscapeModel(this.name, this.country, this.park, this.assetPath);
}

Now we have the model, but what about the data? Let’s create an instance for retrieving the data before we start to store users’ data with .

Creating data source

You need a data source in general. For example, your data source might be a list of messages, search results, or products in a store. Most of the time, this data comes from the internet or a database.

But we will do that later. Now just set up the data with hard-coded strings like this:

...
static List<LandscapeModel> get getModels {
return [
LandscapeModel("Lake Tekapo", "New Zealand", "Lake Tekapo Regional Park",
"assets/LakeTekapo.JPG"),
LandscapeModel("Mt. Cook", "New Zealand", "Aoraki/Mount Cook National Park",
"assets/MtCook.JPG"),
];
}
}

Don’t forget to add the image of Mt. Cook to the assets folder ~

Create a container widget for list item

The first widget you’ll build in this story is a container to display details about each landscape. This container will stores all the widgets showing information for the landmark it displays, so that we can use this widget to display any landscape we want. Later, you’ll combine multiple with to show the landscapes we want to share with our users.

Create a in folder. Create a class for this widget.

For sure we need to access the model of the landscape the widget should show. Add a variable with modifier.

final LandscapeModel model;

Then create constructor for final fields.

const LandscapeItem(this.model);

Let’s embed the widgets with a . Here we set the property to so that the widgets inside will automatically align to left.

@override
Widget build(BuildContext context) {
return Column(children: [
Text(model.name),
Text("${model.park} - ${model.country}")
], crossAxisAlignment: CrossAxisAlignment.start);
}

It works!

Customize the container

Now we have the container and two widgets inside. But shall we decorate the widget and add a thumbnail to the container showing the image of the landscape?

Wrap the With a and add a image to the :

@override
Widget build(BuildContext context) {
return Row(children: [
Image.asset(model.assetPath, width: 48, height: 48, fit: BoxFit.cover),
Column(children: [
Text(model.name),
Text("${model.park} - ${model.country}")
], crossAxisAlignment: CrossAxisAlignment.start)
]);
}

But the layout right now is not so pleased…

They need distance… don’t they?

Give them paddings!

Let me see… the image could be shaped in circle for better visual effects!

Let’s create a widget in :

class RoundImage extends StatelessWidget {
final ImageProvider<Object> img;
final double width;
final double height;
final double radius;
const RoundImage({@required this.img,
this.width = 24,
this.height = 24,
this.radius = 4});
@override
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
decoration: new BoxDecoration(
image: new DecorationImage(
image: img,
fit: BoxFit.cover,
),
borderRadius:
new BorderRadius.all(new Radius.circular(radius)),
),
);
}
}

and apply the widget to our list item and don’t forget to add paddings to the item:

@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(4),
child: Row(children: [
RoundImage(
img: AssetImage(model.assetPath),
width: 48,
height: 48,
radius: 8),
SizedBox(width: 8),
Column(children: [
Text(model.name),
Text("${model.park} - ${model.country}")
], crossAxisAlignment: CrossAxisAlignment.start)
]));
}

and run the application~

Awesome!

Now we get everything we need, why not apply these widgets to a ?

Create the List of Landmarks

Displaying lists of data is a fundamental pattern for mobile apps. Flutter includes the widget to make working with lists a breeze.

Create a new file and create a class:

class ListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title:
Text("Landscape List", style: TextStyle(color: Colors.black)),
brightness: Brightness.light,
backgroundColor: Colors.white,
centerTitle: false),
body: ListPageContent());
}
}

Convert the data source into widgets

To display the list of strings, render each String as a widget using . In this example, display each Landscape on its own line.

For the :

class ListPageContent extends StatelessWidget {
final List<LandscapeModel> models = LandscapeModel.getModels;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: double.infinity,
padding: EdgeInsets.symmetric(vertical: 8),
child: ListView.builder(
itemCount: models.length,
itemBuilder: (BuildContext context, int index) {
return LandscapeItem(models[index]);
}));
}
}

As what you see in the code, we use a with its method to show the list. (The standard constructor works well for small lists. To work with lists that contain a large number of items, it’s best to use the constructor.)

In contrast to the default constructor, which requires creating all items at once, the constructor creates items as they’re scrolled onto the screen.

Inside the constructor, we need to fill two property: and .

Let’s add some more landscapes before running the application:

static List<LandscapeModel> get getModels {
return [
LandscapeModel("Lake Tekapo", "New Zealand", "Lake Tekapo Regional Park",
"assets/LakeTekapo.JPG"),
LandscapeModel("Mt Cook", "New Zealand", "Aoraki/Mount Cook National Park",
"assets/MtCook.JPG"),
LandscapeModel("Seville Plaza de España", "Spain", "Plaza de España",
"assets/Seville-Plaza-de-España.JPG"),
LandscapeModel("Whampoa Pagoda", "China", "Whampoa Pagoda",
"assets/WhampoaPagoda.JPG"),
];
}

and run the application!

Set Up Navigation Between List and Detail

The list renders properly, but you can’t tap an individual landmark to see that landmark’s detail page yet.

You add navigation capabilities to a list by using . You can use a or simply use a to listen to gestures.

void openDetailPage(BuildContext context, int index) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => DetailPage(index)),
);
}

Adding gesture detection to widgets

To listen to gestures from the widgets layer, use a .

If you’re using Material Components, many of those widgets already respond to taps or gestures. For example, and respond to presses (taps), and responds to swipes to trigger scrolling. If you are not using those widgets, but you want the “ink splash” effect on a tap, you can use .

Notice that if you use a , Flutter will give a ripple effect on tap. follows the Material Design guidelines display a ripple animation when tapped.

Flutter provides the widget to perform this effect. Create a ripple effect using the following steps:

  1. Create a widget that supports tap.
  2. Wrap it in an widget to manage tap callbacks and ripple animations.

For example:

// The InkWell wraps the custom flat button widget.
InkWell(
// When the user taps the button, show a snackbar.
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('Tap'),
));
},
child: Container(
padding: EdgeInsets.all(12.0),
child: Text('Flat Button'),
),
);

Let’s set the property of to navigate between the pages.

InkWell(child: LandscapeItem(models[index]), onTap: () {
openDetailPage(context, index);
});

Pass Index into Child Widgets

The widget still uses hard-coded details to show its landscape. Now we need to pass index into child widgets.

Add final field index first to :

final int index;
final List<LandscapeModel> models = LandscapeModel.getModels;
DetailPage(this.index);

And final field model to :

final LandscapeModel model;const DetailPageContent(this.model);

Then pass the model into :

...
body: DetailPageContent(models[index])
...

Now, we just need to change all the hard-coded strings with the model.

@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: double.infinity,
decoration: new BoxDecoration(
image: DecorationImage(
colorFilter: new ColorFilter.mode(
Colors.white.withOpacity(0.8), BlendMode.dstATop),
fit: BoxFit.cover,
image: AssetImage(model.assetPath)),
),
child: Column(children: [
Container(
color: Colors.white,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(children: [
Text(model.park, style: TextStyle(fontSize: 14)),
Spacer(),
Text(model.country, style: TextStyle(fontSize: 14))
]))),
Spacer(),
]));
}

And now we can navigate between pages easily!

Here we go~

In the next story, we will then talk about Handling User Input.

In the Landscape app, a user can flag their favorite places, and filter the list to show just their favorites. To create this feature, you’ll start by adding a switch to the list so users can focus on just their favorites, and then you’ll add a star-shaped button that a user taps to flag a landmark as a favorite.

Thanks for reading~

Cheers ψ(`∇´)ψ~ 这里是苏苏的企鹅~