5
Scrollable Widgets
Written by Vincent Ngo
Building scrollable content is an essential part UI development. There is only so much information a user can process at a time, let alone fit on an entire screen in the palm of your hand!
In this chapter you will learn everything about scrollable widgets. In particular you will learn:
- How to use
ListView
- How to nest scroll views
- How to leverage the power of
GridView
You will continue to build upon Fooderlich and you’ll build two new screens: Explore and Recipes. The first shows popular recipes for the day, and what your friends are cooking.
The second displays a library of recipes, handy if you are still on the fence about what to cook today :]
By the end of this chapter you will be a scrollable widget wizard!
Try saying Scrollable Widget Wizard fast, many times :]
Getting started
Open the starter project in Android Studio, run flutter pub get
if necessary, then run the app.
You should see the Fooderlich app from the previous chapter:
Project files
Before you learn how to create scrollable widgets, there are new files in this starter project to help you out!
Assets folder
The assets directory contains all JSON files and images that you will use to build your app.
Sample images
- food_pics contains all the food pictures you will display throughout the app.
- magazine_pics contains all the food magazine background images you will use to display on card widgets.
- profile_pics contains raywenderlich.com team member pictures.
JSON Data
The sample_data directory contains three JSON files:
New classes
In the lib directory, you will also notice three new folders as shown below:
API folder
The api folder contains a mock service class.
Models folder
There are six model objects you will use to build your app’s UI:
Components folder
All the custom widgets are organized into the lib/components folder.
static List<Widget> pages = <Widget>[
Card1(
recipe: ExploreRecipe(
authorName: "Ray Wenderlich",
title: "The Art of Dough",
subtitle: "Editor's Choice",
message: "Learn to make the perfect bread.",
backgroundImage: "assets/magazine_pics/mag1.jpg")),
Card2(
recipe: ExploreRecipe(
authorName: "Mike Katz",
role: "Smoothie Connoisseur",
profileImage: "assets/profile_pics/person_katz.jpeg",
title: "Recipe",
subtitle: "Smoothies",
backgroundImage: "assets/magazine_pics/mag2.png")),
Card3(
recipe: ExploreRecipe(
title: "Vegan Trends",
tags: [
"Healthy", "Vegan", "Carrots", "Greens", "Wheat",
"Pescetarian", "Mint", "Lemongrass",
"Salad", "Water"
],
backgroundImage: "assets/magazine_pics/mag3.png")),
];
Introducing ListView
ListView is a very popular Flutter component. It’s a linear scrollable widget that arranges its children linearly and supports horizontal and vertical scrolling.
Constructors
A ListView
has four constructors:
Create ExploreScreen
The first screen you will create is the ExploreScreen
. It contains two sections.
import 'package:flutter/material.dart';
import '../api/mock_fooderlich_service.dart';
import '../components/components.dart';
class ExploreScreen extends StatelessWidget {
// 1
final mockService = MockFooderlichService();
@override
Widget build(BuildContext context) {
// 2
// TODO 1: Add TodayRecipeListView FutureBuilder
return Center(
child: Text("Explore Screen"));
}
}
Setup bottom navigation bar
Open home.dart and replace BottomNavigationBar’s items
with the following:
BottomNavigationBarItem(icon: Icon(Icons.explore), label: 'Explore'),
BottomNavigationBarItem(icon: Icon(Icons.book), label: 'Recipes'),
BottomNavigationBarItem(icon: Icon(Icons.list), label: 'To Buy'),
Update the navigation pages
In home.dart replace the pages
property with the following:
static List<Widget> pages = <Widget>[
ExploreScreen(),
// TODO: Replace with RecipesScreen
Container(color: Colors.green),
Container(color: Colors.blue)
];
import 'screens/explore_screen.dart';
Creating a FutureBuilder
How do you display your UI with an asynchronous task?
// 1
return FutureBuilder(
// 2
future: mockService.getExploreData(),
// 3
builder: (context, snapshot) {
// TODO: Add Nested List Views
// 4
if (snapshot.connectionState == ConnectionState.done) {
// 5
var recipes = snapshot.data.todayRecipes;
// TODO: Replace this with TodayRecipeListView
return Center(
child: Container(
child: Text("Show TodayRecipeListView")));
} else {
// 6
return Center(
child: CircularProgressIndicator());
}
});
Building Recipes of the Day 🍳
The first scrollable component you will build is TodayRecipeListView
. This is the top section of the ExploreScreen
. This is going to be a horizontal list view!
import 'package:flutter/material.dart';
// 1
import '../components/components.dart';
import '../models/models.dart';
class TodayRecipeListView extends StatelessWidget {
// 2
final List<ExploreRecipe> recipes;
const TodayRecipeListView({Key key, this.recipes})
: super(key: key);
@override
Widget build(BuildContext context) {
// 3
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
// 4
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 5
Text(
"Recipes of the Day 🍳",
style: Theme.of(context).textTheme.headline1),
// 6
SizedBox(height: 16),
// 7
Container(
height: 400,
// TODO: Add ListView Here
color: Colors.grey,
)
]
)
);
}
}
Add TodayRecipeListView
Open components.dart and add the following export:
export 'today_recipe_list_view.dart';
return TodayRecipeListView(recipes: recipes);
Adding the ListView
In today_recipe_list_view.dart, replace the comment // TODO: Add ListView Here
with the following:
// 1
color: Colors.transparent,
// 2
child: ListView.separated(
// 3
scrollDirection: Axis.horizontal,
// 4
itemCount: recipes.length,
// 5
itemBuilder: (context, index) {
// 6
var recipe = recipes[index];
return buildCard(recipe);
},
// 7
separatorBuilder: (context, index) {
// 8
return SizedBox(width: 16);
})
buildCard(ExploreRecipe recipe) {
if (recipe.cardType == RecipeCardType.card1) {
return Card1(recipe: recipe);
} else if (recipe.cardType == RecipeCardType.card2) {
return Card2(recipe: recipe);
} else if (recipe.cardType == RecipeCardType.card3) {
return Card3(recipe: recipe);
} else {
throw Exception("This card doesn't exist yet");
}
}
Nested ListViews
There are two approaches to building the bottom section.
Column Approach
You could put the two list views in a Column
widget. A Column
widgets arranges items in a vertical layout. This makes sense right?
Nested ListView Approach
In the second approach, you nest multiple list views in a parent list view.
Adding Nested ListView
First open explore_screen.dart and replace the build()
method with the following:
@override
Widget build(BuildContext context) {
// 1
return FutureBuilder(
// 2
future: mockService.getExploreData(),
// 3
builder: (context, snapshot) {
// 4
if (snapshot.connectionState == ConnectionState.done) {
// 5
return ListView(
// 6
scrollDirection: Axis.vertical,
children: [
// 7
TodayRecipeListView(recipes: snapshot.data.todayRecipes),
// 8
SizedBox(height: 16),
// 9
// TODO: Replace this with FriendPostListView
Container(height: 400, color: Colors.green)
]
);
} else {
// 10
return Center(child: CircularProgressIndicator());
}
}
);
}
Creating FriendPostTile
First, you’ll create the items for the list view to display. Below is the FriendPostTile
widget you will create:
import 'package:flutter/material.dart';
import '../models/models.dart';
import '../components/components.dart';
class FriendPostTile extends StatelessWidget {
final Post post;
const FriendPostTile({Key key, this.post}) : super(key: key);
@override
Widget build(BuildContext context) {
// 1
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
// 2
CircleImage(AssetImage(post.profileImageUrl),
imageRadius: 20),
// 3
SizedBox(width: 16),
// 4
Expanded(
child: Container(
// 5
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 6
Text(post.comment),
// 7
Text("${post.timestamp} mins ago",
style: TextStyle(fontWeight: FontWeight.w700))
])))
]);
}
}
export 'friend_post_tile.dart';
Creating FriendPostListView
In the lib/components directory create a new file called friend_post_list_view.dart and add the following code:
import 'package:flutter/material.dart';
import '../models/models.dart';
import 'components.dart';
class FriendPostListView extends StatelessWidget {
// 1
final List<Post> friendPosts;
const FriendPostListView({Key key, this.friendPosts}) : super(key: key);
@override
Widget build(BuildContext context) {
// 2
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 0),
// 3
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 4
Text(
"Social Chefs 👩🍳",
style: Theme.of(context).textTheme.headline1),
// 5
SizedBox(height: 16),
// TODO: Add PostListView here
// 6
SizedBox(height: 16),
]));
}
}
// 1
ListView.separated(
// 2
primary: false,
// 3
physics: NeverScrollableScrollPhysics(),
// 4
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemCount: friendPosts.length,
itemBuilder: (context, index) {
// 5
var post = friendPosts[index];
return FriendPostTile(post: post);
},
separatorBuilder: (context, index) {
// 6
return SizedBox(height: 16);
}),
export 'friend_post_list_view.dart';
Final touches for ExploreScreen
Open explore_screen.dart and replace the code below the comment // TODO: Replace this with FriendPostListView
with the following:
FriendPostListView(friendPosts: snapshot.data.friendPosts)
GridView
GridView
is a 2D array of scrollable widgets. It arranges the children in a grid and supports horizontal and vertical scrolling.
Constructors
Getting used to GridView
is easy. Like ListView
, it inherits from ScrollView
, so their constructors are very similar.
Key parameters
Here are some parameters you should pay attention to:
What’s cross and main axis?
You may be wondering what is the difference between main axis and cross axis! Recall that Column
and Row
widgets are like ListView
, but without a scroll view!
Grid delegates
Grid delegates help figure out the spacing and the number of columns to use to layout the children to a GridView
.
Recipes Screen
You are now ready to build the recipes screen! Within the screens directory create a new file called recipes_screen.dart. Add the following code:
import 'package:flutter/material.dart';
import '../api/mock_fooderlich_service.dart';
import '../components/components.dart';
class RecipesScreen extends StatelessWidget {
// 1
final exploreService = MockFooderlichService();
@override
Widget build(BuildContext context) {
// 2
return FutureBuilder(
// 3
future: exploreService.getRecipes(),
builder: (context, snapshot) {
// 4
if (snapshot.connectionState == ConnectionState.done) {
// TODO: Add RecipesGridView Here
// 5
return Center(child: Text("Recipes Screen"));
} else {
// 6
return Center(child: CircularProgressIndicator());
}
});
}
}
static List<Widget> pages = <Widget>[
ExploreScreen(),
RecipesScreen(),
Container(color: Colors.blue)
];
import 'screens/recipes_screen.dart';
Creating the Recipe Thumbnail
Before you create the grid view, you need a widget to display in the grid! Here is the thumbnail widget you will create:
import 'package:flutter/material.dart';
import '../models/models.dart';
class RecipeThumbnail extends StatelessWidget {
// 1
final SimpleRecipe recipe;
const RecipeThumbnail({Key key, this.recipe}) : super(key: key);
@override
Widget build(BuildContext context) {
// 2
return Container(
padding: EdgeInsets.all(8),
// 3
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 4
Expanded(
child: Container(
// 5
child: ClipRRect(
child: Image.asset("${recipe.dishImage}",
fit: BoxFit.cover),
borderRadius: BorderRadius.circular(12)))),
// 6
SizedBox(height: 10),
// 7
Text(
recipe.title,
maxLines: 1,
style: Theme.of(context).textTheme.bodyText1),
Text(
recipe.duration,
style: Theme.of(context).textTheme.bodyText1)
]
)
);
}
}
export 'recipe_thumbnail.dart';
Creating RecipesGridView
Within the lib/components directory create a new file called recipes_grid_view.dart and add the following code:
import 'package:flutter/material.dart';
import '../components/components.dart';
import '../models/models.dart';
class RecipesGridView extends StatelessWidget {
// 1
final List<SimpleRecipe> recipes;
const RecipesGridView({Key key, this.recipes}) : super(key: key);
@override
Widget build(BuildContext context) {
// 2
return Padding(
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
// 3
child: GridView.builder(
// 4
itemCount: recipes.length,
// 5
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemBuilder: (context, index) {
// 6
var simpleRecipe = recipes[index];
return RecipeThumbnail(recipe: simpleRecipe);
}));
}
}
export 'recipes_grid_view.dart';
Adding RecipesGridView
Open up recipes_screen.dart and replace the return
statement below the comment // TODO: Add RecipesGridView Here
with the following:
return RecipesGridView(recipes: snapshot.data);
Other Scrollable Widgets
There are many more scrollable widgets for various different use cases. Here are some not covered in this chapter:
Challenges
Challenge 1: Add a scroll listener
So far, you’ve built a number of scrollable widgets, but how do you listen to scroll events?
Solution
First you need to make ExploreScreen
a StatefulWidget
. That is because you need to preserve the state of the scroll controller.
ScrollController _controller;
_scrollListener() {
// 1
if (_controller.offset >= _controller.position.maxScrollExtent &&
!_controller.position.outOfRange) {
print("i am at the bottom!");
}
// 2
if (_controller.offset <= _controller.position.minScrollExtent &&
!_controller.position.outOfRange) {
print("i am at the top!");
}
}
@override
void initState() {
// 1
_controller = ScrollController();
// 2
_controller.addListener(_scrollListener);
super.initState();
}
return ListView(
controller: _controller,
...
Challenge 2: New GridView Layout
Try using SliverGridDelegateWithMaxCrossAxisExtent
to create the grid layout below, which displays recipes only in one column:
Solution
In recipes_grid_view.dart, replace the gridDelegate
parameter with the following:
SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 500),
Key points
-
ListView
andGridView
support both horizontal and vertical scroll directions. - The
primary
property lets Flutter know which scroll view is the primary scroll view. - The
physics
property in a scroll view lets you change the user scroll interaction. - Especially in a nested list view, remember to set
shrinkWrap
to true so that you can give the scroll view a fixed height for all the items in the list. - Use a
FutureBuilder
to wait for an async task to complete. - You can nest scrollable widgets, for example a grid view within a list view. Unleash your wildest imagination!
- Use
ScrollController
andScrollNotification
to control or listen to scroll behavior. - Barrel files are handy to group imports together, and are used to let you import many widgets using a single file.
Where to go from here?
You have learned how to create ListView
s and GridView
s. They are much easier to use than iOS’s UITableView
and Android’s RecyclerView
right? Building scrollable widgets is an important skill you should master!