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 the Fooderlich app from the previous chapter:
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 for a sample JSON file to be read and decoded 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
asyncworks in Dart? Check out the asynchronous chapter in Dart Apprentice 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
ExploreRecipes and a list ofPosts. - 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.
Open home.dart and check out pages.
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')),
];
As you can see above, 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!
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:
ColumnandRowwidgets are likeListViewbut 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 anIndexedWidgetBuilderand 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 twoIndexedWidgetBuilders:itemBuilderandseperatorBuilder. 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 ExploreScreen
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 directory, create a new file called explore_screen.dart and add the following code:
import 'package:flutter/material.dart';
import '../api/mock_fooderlich_service.dart';
import '../components/components.dart';
class ExploreScreen extends StatelessWidget {
// 1
final mockService = MockFooderlichService();
ExploreScreen({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 2
// TODO 1: 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.
Setting up the bottom navigation bar
Open home.dart and replace BottomNavigationBar’s items with the following:
const BottomNavigationBarItem(
icon: Icon(Icons.explore), label: 'Explore'),
const BottomNavigationBarItem(
icon: Icon(Icons.book), label: 'Recipes'),
const BottomNavigationBarItem(
icon: Icon(Icons.list), label: 'To Buy'),
Here, you’re just updating the icons and the labels of each BottomNavigationBarItem.
Updating the navigation pages
In home.dart, replace pages with the following:
static List<Widget> pages = <Widget>[
ExploreScreen(),
// TODO: Replace with RecipesScreen
Container(color: Colors.green),
Container(color: Colors.blue)
];
This will display the newly created ExploreScreen in the first tab.
Make sure the new ExploreScreen has 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:
Note: Perform a hot restart, or fully restart the app, if you don’t see the three tabs as above.
You’ll replace the Containers 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 return statement below the comment // TODO 1: Add TodayRecipeListView FutureBuilder and the existing return statement with the following code:
// 1
return FutureBuilder(
// 2
future: mockService.getExploreData(),
// 3
builder: (context, snapshot) {
// TODO: Add Nested List Views
// 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
FutureBuildertakes in aFutureas a parameter.getExploreData()creates a future that will, in turn, return anExploreDatainstance. That instance will contain two lists,todayRecipesandfriendPosts. -
Within
builder, you usesnapshotto check the current state of theFuture. -
Now, the
Futureis complete and you can extract the data to pass to your widget. -
snapshot.datareturnsExploreData, from which you extracttodayRecipesto pass to the list view. Right now, you show a simple text as placeholder. You’ll build aTodayRecipeListViewsoon. -
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
FutureBuilderdocumentation: 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({Key key, this.recipes})
: super(key: key);
@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,
)
]
)
);
}
}
Here’s how the code works:
- Import the barrel files, component.dart and models.dart, so you can use data models and UI components.
-
TodayRecipeListViewneeds a list of recipes to display. - Within
build(), start by applying some padding. - Add a
Columnto 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 TodayRecipeListView
Open 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, open 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.
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
final recipe = recipes[index];
return buildCard(recipe);
},
// 7
separatorBuilder: (context, index) {
// 8
return const SizedBox(width: 16);
})
Make sure to delete the existing color of the container.
Here’s how the code works:
- Change the color from grey to transparent.
- Create
ListView.separated. Remember, this widget creates twoIndexedWidgetBuilders. - Set the scroll direction to the
horizontalaxis. - Set the number of items in the list view.
- Create the
itemBuildercallback, which will go through every item in the list. - Get the recipe for the current index and build the card.
- Create the
separatorBuildercallback, which will go through every item in the list. - For every item, you create a
SizedBoxto space every item 16 points apart.
Next, you need to actually build the card. just below build(), add 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.
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:
-
TodayRecipeListViewis OK because the scroll is in the horizontal direction. All the cards also fit on screen and look great! -
FriendPostListViewscrolls 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 Cards 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 ListViews, 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
TodayRecipeListViewin the horizontal direction. - When you scroll upward, Flutter actually listens to the scroll event of the parent
ListView. So it will scroll bothTodayRecipeListViewandFriendPostListViewupwards, giving you more room to view all the content!
Nested ListView sounds like a better approach, doesn’t it?
Adding the Nested ListView
First, open explore_screen.dart and replace build() 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
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
FutureBuilderfrom before. It runs an asynchronous task and lets you know the state of the future. -
Use your mock service to call
getExploreData(). This returns anExploreDataobject future. -
Check the state of the future within the
buildercallback. -
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 primaryListViewwill hold the other twoListViews as children. -
Set the scroll direction to vertical, although that’s the default value.
-
The first item in
childrenisTodayRecipeListView. You pass in the list oftodayRecipesfromExploreData. -
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
FriendPostListViewlater. -
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 Cards 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 FriendPostListView
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 FriendPostTile
Within lib/components, create a new file called friend_post_tile.dart. Add the following code:
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(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
Rowto 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
Columnto arrange the widgets vertically. - Create a
Textto display your friend’s comments. - Create another
Textto display the timestamp of a post.
Note: There’s no
heightrestriction 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({Key key, this.friendPosts}) : super(key: key);
@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:
-
FriendPostListViewrequires a list ofPosts. - Apply a left and right padding widget of 16 points.
- Create a
Columnto position theTextfollowed by the posts in a vertical layout. - Create the
Textwidget 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.separatedwith twoIndexWidgetBuildercallbacks. - Since you’re nesting two list views, it’s a good idea to set
primaryto false. That lets Flutter know that this isn’t the primary scroll view. - Set the scrolling physics to
NeverScrollableScrollPhysics. Even though you setprimaryto 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
shrinkWraptotrueto 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
SizedBoxto space each item by 16 points.
Note: There are several different types of scroll physics you can play with:
AlwaysScrollableScrollPhysicsBouncingScrollPhysicsClampingScrollPhysicsFixedExtentScrollPhysicsNeverScrollableScrollPhysicsPageScrollPhysicsRangeMaintainingScrollPhysicsFind more details at https://api.flutter.dev/flutter/widgets/ScrollPhysics-class.html.
Open components.dart and add the following export:
export 'friend_post_list_view.dart';
And that’s it. Now, you’ll just finish up ExploreScreen and your app will have a cool new feature!
Adding 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),
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
GridViewhas 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 Columns and Rows are like ListViews, 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:
SliverGridDelegateWithFixedCrossAxisCountSliverGridDelegateWithMaxCrossAxisExtent
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 '../api/mock_fooderlich_service.dart';
import '../components/components.dart';
class RecipesScreen extends StatelessWidget {
// 1
final exploreService = MockFooderlichService();
RecipesScreen({Key key}) : super(key: key);
@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 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 ofSimpleRecipes. -
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, replace pages with:
static List<Widget> pages = <Widget>[
ExploreScreen(),
RecipesScreen(),
Container(color: Colors.blue)
];
Next, add the following import:
import 'screens/recipes_screen.dart';
Perform a hot restart or rebuild and run the app to see the start 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({Key key, this.recipe}) : super(key: key);
@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(
child: Image.asset('${recipe.dishImage}',
fit: BoxFit.cover),
borderRadius: BorderRadius.circular(12))),
// 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
SimpleRecipeas a parameter. That helps configure your widget. - Create a
Containerwith 8-point padding all around. - Use a
Columnto apply a vertical layout. - The first element of the column is
Expanded. That widget holds on to aContainer, which will then hold on to yourImage. You want the image to fill the remaining space. - The
Imageis within theClipRRect, which clips the image to make the borders rounded. - Add some room between the image and the other widgets.
- Add the remaining
Texts: 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 RecipesGridView
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({Key key, this.recipes}) : super(key: key);
@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:
-
RecipesGridViewrequires 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
SliverGridDelegateWithFixedCrossAxisCountand set thecrossAxisCountto 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';
Adding RecipesGridView
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:
There are now two unused import statements. Open home.dart and remove the following:
import 'models/explore_recipe.dart';
import 'components/components.dart';
And that’s it, you’re done. Congratulations!
Other scrollable widgets
There are many more scrollable widgets for various use cases. Here are some not covered in this chapter:
- PageView: A scrollable widget that scrolls page by page, making it perfect for an onboarding flow. It also supports a vertical scroll direction.
- CustomScrollView: A widget that creates custom scroll effects using slivers. Ever wonder how to collapse your navigation header on scroll? Slivers and custom scroll views will do that!
- 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 a number of 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:
-
print('i am at the bottom!')if the user scrolls to the bottom. -
print('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
ExploreScreena stateful widget.- Create an instance of
ScrollControllerin 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
shrinkWrapto 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 ListViews and GridViews. 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.