5.
Scrollable Widgets
Written by Vincent Ngo
Building scrollable content is an essential part of UI development. There’s 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’ll learn everything you need to know about scrollable widgets. In particular, you’ll learn:
- How to use
ListView
. - How to nest scroll views.
- How to leverage the power of
GridView
.
You’ll continue to build your recipe app, Fooderlich, by adding two new screens: Explore and Recipes. The first shows popular recipes for the day along with what your friends are cooking.
The second displays a library of recipes, handy if you’re still on the fence about what to cook today. :]
By the end of this chapter, you’ll be a scrollable widget wizard!
Getting started
Open the starter project in Android Studio, then run flutter pub get
if necessary and run the app.
You’ll see colored screens as shown below:
Project files
There are new files in this starter project to help you out. Before you learn how to create scrollable widgets, take a look at them.
Assets folder
The assets directory contains all JSON files and images that you’ll use to build your app.
Sample images
- food_pics: Contains the food pictures you’ll display throughout the app.
- magazine_pics: All the food magazine background images you’ll display on card widgets.
- profile_pics: Contains raywenderlich.com team member pictures.
JSON Data
The sample_data directory contains three JSON files:
- sample_explore_recipes.json: A list of recipes to display on the home screen. Sometimes, users might want recommendations for what to cook today!
- sample_friends_feed.json: This list contains samples of your friends’ posts, in case you’re curious about what your friends are cooking up! 👩🍳
- sample_recipes.json: A list of recipes including details about the duration and cooking difficulty of each.
New classes
In the lib directory, you’ll also notice three new folders, as shown below:
API folder
The api folder contains a mock service class.
MockFooderlichService
is a service class that mocks a server response. It has async
functions that wait to load data from a sample JSON and decode it to recipe model objects.
In this chapter, you’ll use two API calls:
-
getExploreData(): Returns
ExploreData
. Internally, it makes a batch request and returns two lists: recipes to explore and friend posts. - getRecipes(): Returns the list of recipes.
Note: Unfamiliar with how
async
works in Dart? Check out Chapter 10, “Asynchronous Programming” in Dart Apprentice https://www.raywenderlich.com/books/dart-apprentice/v1.1/chapters/10-asynchronous-programming or read this article to learn more: https://dart.dev/codelabs/async-await.
Pro tip: Sometimes your back-end service is not ready to consume. Creating a mock service object is a flexible way to build your UI. Instead of creating many recipe mock objects, all you have to do is change a JSON file.
Models folder
You’ll use these six model objects to build your app’s UI:
- ExploreRecipe: All of the details about a recipe. It contains ingredients, instructions, duration and a whole lot more.
-
Ingredient: A single ingredient. This is part of
ExploreRecipe
. -
Instruction: A single instruction to cook the recipe. It’s part of
ExploreRecipe
. - Post: Describes a friend’s post. A post is similar to a tweet and represents what your social network is cooking.
-
ExploreData: Groups two datasets. It contains a list of
ExploreRecipe
s and a list ofPost
s. - SimpleRecipe: How difficult a recipe is to cook.
Feel free to explore the different properties each model object contains!
Note: models.dart is a barrel file. It exports all your model objects and makes it convenient to import them later on. Think of this as grouping many imports into a single file.
Components folder
lib/components contains all your custom widgets.
Note: components.dart is another barrel file that groups all imports in a single file.
Notice that every single Card
now requires an ExploreRecipe
instance.
That’s it for getting up to speed on the new starter project files!
Now that you have a mock service and model objects, you can focus on scrollable widgets!
Pubspec file
In pubspec.yaml you’ll notice new asset folders:
assets:
- assets/
- assets/food_pics/
- assets/magazine_pics/
- assets/sample_data/
- assets/profile_pics/
With these changes the Fooderlich app can now access all the pictures needed for this chapter.
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.
Fun fact:
Column
andRow
widgets are likeListView
but without the scroll view.
Introducing Constructors
A ListView
has four constructors:
- The default constructor takes an explicit list of widgets called
children
. That will construct every single child in the list, even the ones that aren’t visible. You should use this if you have a small number of children. -
ListView.builder()
takes in anIndexedWidgetBuilder
and builds the list on demand. It will only construct the children that are visible onscreen. You should use this if you need to display a large or infinite number of items. -
ListView.separated()
takes twoIndexedWidgetBuilder
s:itemBuilder
andseperatorBuilder
. This is useful if you want to place a separator widget between your items. -
ListView.custom()
gives you more fine-grain control over your child items.
Note: For more details about ListView constructors, check out the official documentation: https://api.flutter.dev/flutter/widgets/ListView-class.html
Next, you’ll learn how to use the first three constructors!
Setting up the Explore screen
The first screen you’ll create is the ExploreScreen
. It contains two sections:
- TodayRecipeListView: A horizontal scroll view that lets you pan through different cards.
- FriendPostListView: A vertical scroll view that shows what your friends are cooking.
In the lib folder, create a new directory called screens.
Within the new screens directory, create a new file called explore_screen.dart and add the following code:
import 'package:flutter/material.dart';
import '../components/components.dart';
import '../models/models.dart';
import '../api/mock_fooderlich_service.dart';
class ExploreScreen extends StatelessWidget {
// 1
final mockService = MockFooderlichService();
ExploreScreen({super.key});
@override
Widget build(BuildContext context) {
// 2
// TODO: Add TodayRecipeListView FutureBuilder
return const Center(
child: Text('Explore Screen'),
);
}
}
Here’s how the code works:
- Create a
MockFooderlichService
, to mock server responses. - Display a placeholder text. You’ll replace this later.
Leave explore_screen.dart open; you’ll soon be making some updates.
Updating the navigation pages
In lib/home.dart, locate // TODO: Replace with ExploreScreen
and replace the line below with the following:
ExploreScreen(),
This will display the newly created ExploreScreen
in the first tab.
Make sure the new ExploreScreen
has been imported. If your IDE didn’t add it automatically, add this import:
import 'screens/explore_screen.dart';
Hot restart the app. It will look like this:
You’ll replace the Container
s later in this chapter.
Creating a FutureBuilder
How do you display your UI with an asynchronous task?
MockFooderlichService
contains asynchronous functions that return a Future
object. FutureBuilder
comes in handy here, as it helps you determine the state of a future. For example, it tells you whether data is still loading or the fetch has finished.
In explore_screen.dart, replace the whole return
statement below // TODO: Add TodayRecipeListView FutureBuilder
with the following code:
// 1
return FutureBuilder(
// 2
future: mockService.getExploreData(),
// 3
builder: (context, AsyncSnapshot<ExploreData> snapshot) {
// 4
if (snapshot.connectionState == ConnectionState.done) {
// 5
final recipes = snapshot.data?.todayRecipes ?? [];
// TODO: Replace this with TodayRecipeListView
return Center(
child: Container(
child: const Text('Show TodayRecipeListView'),
),
);
} else {
// 6
return const Center(
child: CircularProgressIndicator(),
);
}
},
);
Here’s what the code does:
-
Within the widget’s
build()
, you create aFutureBuilder
. -
The
FutureBuilder
takes in aFuture
as a parameter.getExploreData()
creates a future that will, in turn, return anExploreData
instance. That instance will contain two lists,todayRecipes
andfriendPosts
. -
Within
builder
, you usesnapshot
to check the current state of theFuture
. -
Now, the
Future
is complete and you can extract the data to pass to your widget. -
snapshot.data
returnsExploreData
, from which you extracttodayRecipes
to pass to the list view. Right now, you show a simple text as a placeholder. You’ll build aTodayRecipeListView
soon. -
The future is still loading, so you show a spinner to let the user know something is happening.
Note: For more information, check out Flutter’s
FutureBuilder
documentation: https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html.
Perform a hot reload. You’ll see the loading spinner first. After the future completes, it shows the placeholder text.
Now that you’ve set up the loading UI, it’s time to build the actual list view!
Building Recipes of the Day
The first scrollable component you’ll build is TodayRecipeListView
. This is the top section of the ExploreScreen
. It will be a horizontal list view.
In lib/components, create a new file called today_recipe_list_view.dart. Add the following code:
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({
super.key,
required this.recipes,
});
@override
Widget build(BuildContext context) {
// 3
return Padding(
padding: const 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
const SizedBox(height: 16),
// 7
Container(
height: 400,
// TODO: Add ListView Here
color: Colors.grey,
),
],
),
);
}
// TODO: Add buildCard() widget here
}
Here’s how the code works:
- Import the barrel files, component.dart and models.dart, so you can use data models and UI components.
-
TodayRecipeListView
needs a list of recipes to display. - Within
build()
, start by applying some padding. - Add a
Column
to place widgets in a vertical layout. - In the column, add a
Text
. This is the header for the Recipes of the Day. - Add a 16-point-tall
SizedBox
, to supply some padding. - Add a
Container
, 400 points tall, and set the background color to grey. This container will hold your horizontal list view.
Adding ListView for today’s recipes
Open lib/components/components.dart and add the following export:
export 'today_recipe_list_view.dart';
This means you don’t have to call additional imports when you use the new component.
Next, return to explore_screen.dart and replace the return
statement below the comment // TODO: Replace this with TodayRecipeListView
with the following:
return TodayRecipeListView(recipes: recipes);
If your app is still running, it will now look like this:
Now it’s finally time to add the ListView
.
In today_recipe_list_view.dart, replace the comment // TODO: Add ListView Here
and the color:
line below it with the following, ignoring any red squiggles:
// 1
color: Colors.transparent,
// 2
child: ListView.separated(
// 3
scrollDirection: Axis.horizontal,
// 4
itemCount: recipes.length,
// 5
itemBuilder: (context, index) {
// 6
final recipe = recipes[index];
return buildCard(recipe);
},
// 7
separatorBuilder: (context, index) {
// 8
return const SizedBox(width: 16);
},
),
Here’s how the code works:
- Change the color from grey to transparent.
- Create
ListView.separated
. Remember, this widget creates twoIndexedWidgetBuilder
s. - Set the scroll direction to the
horizontal
axis. - Set the number of items in the list view.
- Create the
itemBuilder
callback, which will go through every item in the list. - Get the recipe for the current index and build the card.
- Create the
separatorBuilder
callback, which will go through every item in the list. - For every item, you create a
SizedBox
to space every item 16 points apart.
Next, you need to actually build the card. Replace // TODO: Add buildCard() widget here
with the following:
Widget 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');
}
}
This function builds the card for each item. Every ExploreRecipe
has a cardType
. This helps you determine which Card
to create for that recipe.
Save the change to trigger a hot restart, and Fooderlich will now look like this:
Finally, you can scroll through the list of beautiful recipes for the day. Don’t forget, you can switch the theme in main.dart to dark mode!
Next, you’ll build the bottom section of ExploreScreen
.
Nested ListViews
There are two approaches to building the bottom section: the Column approach and the Nested ListView approach. You’ll take a look at each of them now.
Column approach
You could put the two list views in a Column
. A Column
arranges items in a vertical layout, so that makes sense right?
The diagram shows two rectangular boundaries that represent two scrollable areas.
The pros and cons to this approach are:
-
TodayRecipeListView
is OK because the scroll is in the horizontal direction. All the cards also fit on the screen and look great! -
FriendPostListView
scrolls in the vertical direction, but it only has a small scroll area. So as a user, you can’t see very many of your friend’s posts at once.
This approach has a bad user experience because the content area is too small! The Card
s already take up most of the screen. How much room will there be for the vertical scroll area on small devices?
Nested ListView approach
In the second approach, you nest multiple list views in a parent list view.
The diagram shows one big rectangular boundary.
ExploreScreen
holds the parent ListView
. Since there are only two child ListView
s, you can use the default constructor, which returns an explicit list of children.
The benefits of this approach are:
- The scroll area is a lot bigger, using 70–80% of the screen.
- You can view more of your friends’ posts.
- You can continue to scroll
TodayRecipeListView
in the horizontal direction. - When you scroll upward, Flutter listens to the scroll event of the parent
ListView
. So it will scroll bothTodayRecipeListView
andFriendPostListView
upwards, giving you more room to view all the content!
Nested ListView
sounds like a better approach, doesn’t it?
Adding the nested ListView
First, go back to explore_screen.dart and replace build()
with the following:
@override
Widget build(BuildContext context) {
// 1
return FutureBuilder(
// 2
future: mockService.getExploreData(),
// 3
builder: (context, AsyncSnapshot<ExploreData> snapshot) {
// 4
if (snapshot.connectionState == ConnectionState.done) {
// 5
return ListView(
// 6
scrollDirection: Axis.vertical,
children: [
// 7
TodayRecipeListView(recipes: snapshot.data?.todayRecipes ?? []),
// 8
const SizedBox(height: 16),
// 9
// TODO: Replace this with FriendPostListView
Container(
height: 400,
color: Colors.green,
),
],
);
} else {
// 10
return const Center(child: CircularProgressIndicator());
}
},
);
}
Here’s how the code works:
-
This is the
FutureBuilder
from before. It runs an asynchronous task and lets you know the state of the future. -
Use your mock service to call
getExploreData()
. This returns anExploreData
object future. -
Check the state of the future within the
builder
callback. -
Check if the future is complete.
-
When the future is complete, return the primary
ListView
. This holds an explicit list of children. In this scenario, the primaryListView
will hold the other twoListView
s as children. -
Set the scroll direction to vertical, although that’s the default value.
-
The first item in
children
isTodayRecipeListView
. You pass in the list oftodayRecipes
fromExploreData
. -
Add a 16-point vertical space so the lists aren’t too close to each other.
-
Add a green placeholder container. You’ll create and add the
FriendPostListView
later. -
If the future hasn’t finished loading yet, show a circular progress indicator.
Your app now looks like this:
Notice that you can still scroll the Card
s horizontally. When you scroll up and down, you’ll notice the entire area scrolls!
Now that you have the desired scroll behavior, it’s time to build FriendPostListView
.
Creating the ListView for friends’ posts
First, you’ll create the items for the list view to display. When those are ready, you’ll build a vertical list view to display them.
Here’s how FriendPostTile
will look:
It’s time to get started!
Building the tile for a friend’s post
Within lib/components, create a new file called friend_post_tile.dart. Add the following code:
import 'package:flutter/material.dart';
import '../components/components.dart';
import '../models/models.dart';
class FriendPostTile extends StatelessWidget {
final Post post;
const FriendPostTile({
super.key,
required this.post,
});
@override
Widget build(BuildContext context) {
// 1
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
// 2
CircleImage(
imageProvider: AssetImage(post.profileImageUrl),
imageRadius: 20,
),
// 3
const SizedBox(width: 16),
// 4
Expanded(
// 5
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 6
Text(post.comment),
// 7
Text(
'${post.timestamp} mins ago',
style: const TextStyle(fontWeight: FontWeight.w700),
),
],
),
),
],
);
}
}
Here’s how the code works:
- Create a
Row
to arrange the widgets horizontally. - The first element is a circular avatar, which displays the image asset associated with the
post
. - Apply a 16-point padding.
- Create
Expanded
, which makes the children fill the rest of the container. - Establish a
Column
to arrange the widgets vertically. - Create a
Text
to display your friend’s comments. - Create another
Text
to display the timestamp of a post.
Note: There’s no
height
restriction onFriendPostTile
. That means the text can expand to many lines as long as it’s in a scroll view! This is like iOS’s dynamic table views and autosizing TextViews in Android.
Open components.dart and add the following:
export 'friend_post_tile.dart';
Now, it’s time to create your vertical ListView
.
Creating FriendPostListView
In lib/components, 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({
super.key,
required this.friendPosts,
});
@override
Widget build(BuildContext context) {
// 2
return Padding(
padding: const 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
const SizedBox(height: 16),
// TODO: Add PostListView here
// 6
const SizedBox(height: 16),
],
),
);
}
}
Here’s how the code works:
-
FriendPostListView
requires a list ofPost
s. - Apply a left and right padding widget of 16 points.
- Create a
Column
to position theText
followed by the posts in a vertical layout. - Create the
Text
widget header. - Apply a spacing of 16 points vertically.
- Leave some padding at the end of the list.
Next, add the following code below // TODO: Add PostListView here
:
// 1
ListView.separated(
// 2
primary: false,
// 3
physics: const NeverScrollableScrollPhysics(),
// 4
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemCount: friendPosts.length,
itemBuilder: (context, index) {
// 5
final post = friendPosts[index];
return FriendPostTile(post: post);
},
separatorBuilder: (context, index) {
// 6
return const SizedBox(height: 16);
},
),
Here’s how you defined the new ListView
:
- Create
ListView.separated
with twoIndexWidgetBuilder
callbacks. - Since you’re nesting two list views, it’s a good idea to set
primary
to false. That lets Flutter know that this isn’t the primary scroll view. - Set the scrolling physics to
NeverScrollableScrollPhysics
. Even though you setprimary
to false, it’s also a good idea to disable the scrolling for this list view. That will propagate up to the parent list view. - Set
shrinkWrap
totrue
to create a fixed-length scrollable list of items. This gives it a fixed height. If this were false, you’d get an unbounded height error. - For every item in the list, create a
FriendPostTile
. - For every item, also create a
SizedBox
to space each item by 16 points.
Note: There are several different types of scroll physics you can play with:
AlwaysScrollableScrollPhysics
BouncingScrollPhysics
ClampingScrollPhysics
FixedExtentScrollPhysics
NeverScrollableScrollPhysics
PageScrollPhysicsRange
MaintainingScrollPhysics
Find more details at https://api.flutter.dev/flutter/widgets/ScrollPhysics-class.html.
In components.dart add the following export:
export 'friend_post_list_view.dart';
And that’s it. Now, you’ll finish up the Explore screen and your app will have a cool new feature!
Adding final touches to the Explore screen
Return to explore_screen.dart and replace // TODO: Replace this with FriendPostListView
and the Container
code below it with the following:
FriendPostListView(friendPosts: snapshot.data?.friendPosts ?? []),
Here, you create a FriendPostListView
and extract friendPosts
from ExploreData
.
Restart or hot reload the app. The final Explore screen should look like the following in light mode:
Here’s what it looks like in dark mode:
Aren’t nested scroll views a neat technique? :]
Now, it’s time to play with grid views.
Getting to know GridView
GridView
is a 2D array of scrollable widgets. It arranges the children in a grid and supports horizontal and vertical scrolling.
Getting used to GridView
is easy. Like ListView
, it inherits from ScrollView
, so their constructors are very similar.
GridView
has five types of constructors:
- The default takes an explicit list of widgets.
GridView.builder()
GridView.count()
GridView.custom()
GridView.extent()
The builder()
and count()
constructors are the most common. You’ll have no problem getting used to these since ListView
uses similar ones.
Key parameters
Here are some parameters you should pay attention to:
- crossAxisSpacing: The spacing between each child in the cross axis.
- mainAxisSpacing: The spacing between each child on the main axis.
- crossAxisCount: The number of children in the cross axis. You can also think of this as the number of columns you want in a grid.
- shrinkWrap. Controls the fixed scroll area size.
- physics: Controls how the scroll view responds to user input.
- primary: Helps Flutter determine which scroll view is the primary one.
- scrollDirection: Controls the axis along which the view will scroll.
Note
GridView
has a plethora of parameters to experiment and play with. Check out Greg Perry’s article to learn more: https://medium.com/@greg.perry/decode-gridview-9b123553e604.
Understanding the cross and main axis?
What’s the difference between the main and cross axis? Remember that Column
s and Row
s are like ListView
s, but without a scroll view.
The main axis always corresponds to the scroll direction!
If your scroll direction is horizontal, you can think of this as a Row
. The main axis represents the horizontal direction, as shown below:
If your scroll direction is vertical, you can think of it as a Column
. The main axis represents the vertical direction, as shown below:
Grid delegates
Grid delegates help figure out the spacing and the number of columns to use to lay out the children in a GridView
.
Aside from customizing your own grid delegates, Flutter provides two delegates you can use out of the box:
SliverGridDelegateWithFixedCrossAxisCount
SliverGridDelegateWithMaxCrossAxisExtent
The first creates a layout that has a fixed number of tiles along the cross axis. The second creates a layout with tiles that have a maximum cross axis extent.
Building the Recipes screen
You are now ready to build the Recipes screen! In the screens directory, create a new file called recipes_screen.dart. Add the following code:
import 'package:flutter/material.dart';
import '../models/models.dart';
import '../api/mock_fooderlich_service.dart';
import '../components/components.dart';
class RecipesScreen extends StatelessWidget {
// 1
final exploreService = MockFooderlichService();
RecipesScreen({super.key});
@override
Widget build(BuildContext context) {
// 2
return FutureBuilder(
// 3
future: exploreService.getRecipes(),
builder: (context, AsyncSnapshot<List<SimpleRecipe>> snapshot) {
// 4
if (snapshot.connectionState == ConnectionState.done) {
// TODO: Add RecipesGridView Here
// 5
return const Center(child: Text('Recipes Screen'));
} else {
// 6
return const Center(child: CircularProgressIndicator());
}
},
);
}
}
The code has a similar setup to ExploreScreen
. To create it, you:
-
Create a mock service.
-
Create a
FutureBuilder
. -
Use
getRecipes()
to return the list of recipes to display. This function returns a future list ofSimpleRecipe
s. -
Check if the future is complete.
-
Add a placeholder text until you build
RecipesGridView
. -
Show a circular loading indicator if the future isn’t complete yet.
In home.dart, add the following import
:
import 'screens/recipes_screen.dart';
Next locate the comment // TODO: Replace with RecipesScreen
and replace the Container
beneath it with the following:
RecipesScreen(),
Perform a hot restart to see the first step of the new recipes screen:
Creating the recipe thumbnail
Before you create the grid view, you need a widget to display in the grid. Here’s the thumbnail widget you’ll create:
It’s a simple tile that displays the picture, the name and the duration of a recipe.
In lib/components, create a new file called recipe_thumbnail.dart and add the following code:
import 'package:flutter/material.dart';
import '../models/models.dart';
class RecipeThumbnail extends StatelessWidget {
// 1
final SimpleRecipe recipe;
const RecipeThumbnail({
super.key,
required this.recipe,
});
@override
Widget build(BuildContext context) {
// 2
return Container(
padding: const EdgeInsets.all(8),
// 3
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 4
Expanded(
// 5
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.asset(
recipe.dishImage,
fit: BoxFit.cover,
),
),
),
// 6
const SizedBox(height: 10),
// 7
Text(
recipe.title,
maxLines: 1,
style: Theme.of(context).textTheme.bodyText1,
),
Text(
recipe.duration,
style: Theme.of(context).textTheme.bodyText1,
)
],
),
);
}
}
Here’s how the code works:
- This class requires a
SimpleRecipe
as a parameter. That helps configure your widget. - Create a
Container
with 8-point padding all around. - Use a
Column
to apply a vertical layout. - The first element of the column is
Expanded
, which widget holds on to anImage
. You want the image to fill the remaining space. - The
Image
is within theClipRRect
, which clips the image to make the borders rounded. - Add some room between the image and the other widgets.
- Add the remaining
Text
s: one to display the recipe’s title and another to display the duration.
Next, open components.dart and add the following export:
export 'recipe_thumbnail.dart';
Now, you’re ready to create your grid view!
Creating the recipes GridView
In lib/components, 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({
super.key,
required this.recipes,
});
@override
Widget build(BuildContext context) {
// 2
return Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
top: 16,
),
// 3
child: GridView.builder(
// 4
itemCount: recipes.length,
// 5
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
itemBuilder: (context, index) {
// 6
final simpleRecipe = recipes[index];
return RecipeThumbnail(recipe: simpleRecipe);
},
),
);
}
}
A GridView
is similar to a ListView
. Here’s how it works:
-
RecipesGridView
requires a list of recipes to display in a grid. - Apply a 16 point padding on the left, right, and top.
- Create a
GridView.builder
, which displays only the items visible onscreen. - Tell the grid view how many items will be in the grid.
- Add
SliverGridDelegateWithFixedCrossAxisCount
and set thecrossAxisCount
to 2. That means that there will be only two columns. - For every index, fetch the recipe and create a corresponding
RecipeThumbnail
.
Open components.dart and add the following export:
export 'recipes_grid_view.dart';
Using the recipes GridView
Open recipes_screen.dart and replace the return
statement below the comment // TODO: Add RecipesGridView Here
with the following:
return RecipesGridView(recipes: snapshot.data ?? []);
When the list of recipes has been loaded this will display them in a grid layout.
Congratulations, you’ve now set up your RecipesScreen
!
If you still have your app running, perform a hot reload. The new screen will look like this:
And that’s it, you’re done. Congratulations!
Fun fact: Did you know
ListView
andGridView
widgets are implemented using Slivers? Think of slivers as an interface for scrollable widgets.ListView
inherits fromBoxScrollView
and under the hood is aCustomScrollView
with a singleSliverList
.Check out the implementation for
BoxScrollView
here:Slivers are just part of a scrollable area. A benefit of using slivers is to lazily load items in as it scrolls into view. This makes the list efficient, especially with a large number of children in your list.
To explore more on slivers check out the following links:
Other scrollable widgets
There are many more scrollable widgets for various use cases. Here are some not covered in this chapter:
-
CustomScrollView: A widget that creates custom scroll effects using slivers. Ever wonder how to collapse your navigation header on scroll? Use
CustomScrollView
for more fine-grain control over your scrollable area!
- PageView: A scrollable widget that scrolls page by page, making it perfect for an onboarding flow. It also supports a vertical scroll direction.
- StaggeredGridView: A grid view package that supports columns and rows of varying sizes. If you need to support dynamic height and custom layouts, this is the most popular package.
Now it’s time for some challenges.
Challenges
Challenge 1: Add a scroll listener
So far, you’ve built some scrollable widgets, but how do you listen to scroll events?
For this challenge, try adding a scroll controller to ExploreScreen
. Print two statements to the console:
-
log('i am at the bottom!')
if the user scrolls to the bottom. -
log('i am at the top!')
if the user scrolls to the top.
You can view the scroll controller API documentation here: https://api.flutter.dev/flutter/widgets/ScrollController-class.html.
Here’s a step-by-step hint:
- Make
ExploreScreen
a stateful widget.- Create an instance of
ScrollController
in theinitState()
.- Create
scrollListener()
to listen to the scroll position.- Add a scroll listener to the scroll controller.
- Add the scroll controller to the
ListView
.- Dispose your scroll
scrollListener()
.
Solution
See Appendix A.
Challenge 2: Add a new GridView layout
Try using SliverGridDelegateWithMaxCrossAxisExtent
to create the grid layout below, which displays recipes in only one column:
Solution
See Appendix B.
Key points
- ListView and GridView support both horizontal and vertical scroll directions.
- The primary property lets Flutter know which scroll view is the primary scroll view.
- physics in a scroll view lets you change the user scroll interaction.
- Especially in a nested list view, remember to set
shrinkWrap
to true so you can give the scroll view a fixed height for all the items in the list. - Use a FutureBuilder to wait for an asynchronous task to complete.
- You can nest scrollable widgets. For example, you can place a grid view within a list view. Unleash your wildest imagination!
- Use ScrollController and ScrollNotification to control or listen to scroll behavior.
- Barrel files are handy to group imports together. They also let you import many widgets using a single file.
Where to go from here?
At this point, you’ve 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!
Flutter makes it easy to build and use such scrollable widgets. It offers the flexibility to scroll in any direction and the power to nest scrollable widgets. With the skills you’ve learned, you can build cool scroll interactions.
You’re ready to look like a pro in front of your friends :]
For more examples check out the Flutter Gallery at https://gallery.flutter.dev/#/, which showcases some great examples to test out.
In the next chapter, you’ll take a look at some more interactive widgets.