Building Lists and Navigations — Flutter Tutorial Part 3

Hoarfroster
8 min readJan 13, 2021

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 Navigator.push() :)

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 landscapeModel.dart in models folder. Define a LandscapeModel structure with a few properties matching names of some of the keys that we will use in our widgets. They are name country park and assetPath .

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 SharedPreference .

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 Container with ListView to show the landscapes we want to share with our users.

Create a landscapeItem.dart in views folder. Create a LandscapeItem class for this widget.

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

final LandscapeModel model;

Then create constructor for final fields.

const LandscapeItem(this.model);

Let’s embed the Text widgets with a Column . Here we set the property crossAxisAlignment to CrossAxisAlignment.start 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 Text widgets inside. But shall we decorate the widget and add a thumbnail to the container showing the image of the landscape?

Wrap the Column With a Row and add a image to the Row:

@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 RoundedImage widget in /views/roundedImage.dart :

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 ListView ?

Create the List of Landmarks

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

Create a new file ListPage and create a ListPage 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 ListView.builder(). In this example, display each Landscape on its own line.

For the ListPageContent:

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 ListView with its method builder to show the list. (The standard ListView constructor works well for small lists. To work with lists that contain a large number of items, it’s best to use the ListView.builder constructor.)

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

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

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 Navigator . You can use a InkWell or simply use a GestureDetector to listen to onTap 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 GestureDetector.

If you’re using Material Components, many of those widgets already respond to taps or gestures. For example, IconButton and TextButton respond to presses (taps), and ListView 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 InkWell.

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

Flutter provides the InkWell 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 InkWell 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 onTap of InkWell to navigate between the pages.

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

Pass Index into Child Widgets

The DetailPageContent 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 DetailPage :

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

And final field model to DetailPageContent :

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

Then pass the model into DetailPageContent :

...
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~

--

--