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.
You’ll continue to build the Yummy app by adding HomeScreen, a new view that enables users to explore different restaurants, food categories, and view friends’ posts.
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 a placeholder for each tab 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 the images that you’ll use to build your app.
Sample Images
- categories: Contains images for food categories.
- food: Contains sample food items from a restaurant menu.
- profile_pics: Contains Kodeco team member pictures.
- restaurants: Contains restaurant hero images.
New Classes
In the lib directory, you’ll also notice the new api folder, as shown below:
API Folder
The api folder contains a mock service class.
MockYummyService is a service class that mocks a server response. It has async functions that wait to load mock data defined in each model class, FoodCategory, Post, and Restaurant.
Pro tip: Sometimes your back-end service is not ready to consume. Creating a mock service is a flexible way to build your UI.
In this chapter, you’ll use two API calls:
-
getExploreData(): Returns
ExploreData. Internally, it makes a batch request and returns three lists: restaurants, food categories, and friend posts.
Note: Unfamiliar with how
asyncworks in Dart? Check out Chapter 12, “Futures” in Dart Apprentice: Beyond the Basics or read this article to learn more: https://dart.dev/codelabs/async-await.
Now that you have a mock service, you can focus on displaying the data with 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 the Explore Screen
The first screen you’ll create is the ExploreScreen. It contains three sections:
- RestaurantSection: A horizontal scroll view that lets you pan through different restaurants.
- CategorySection: A horizontal scroll view that pans through different categories.
- PostSection: A vertical scroll view that shows what your friends are up to.
In the lib folder, create a new directory called screens.
Within the new screens directory, create a new file called explore_page.dart and add the following code:
import 'package:flutter/material.dart';
import '../api/mock_yummy_service.dart';
class ExplorePage extends StatelessWidget {
// 1
final mockService = MockYummyService();
ExplorePage({super.key});
@override
Widget build(BuildContext context) {
// TODO: Add Listview Future Builder
// 2
return const Center(
child: Text('Explore Page Setup',
style: TextStyle(fontSize: 32.0),),);
}
}
Here’s how the code works:
- Create a
MockYummyService, to mock server responses. - Display a placeholder text. You’ll replace this later.
Leave explore_page.dart open; you’ll soon be making some changes.
Updating the Navigation Pages
In lib/home.dart, locate // TODO: Replace with ExplorePage and replace Center below it with the following:
ExplorePage(),
This will display the newly created ExplorePage 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_page.dart';
Hot restart the app. It will look like this:
You’ll replace the Containers later in this chapter.
Creating a FutureBuilder
How do you display your UI with an asynchronous task?
MockYummyService 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 operation has finished.
In explore_page.dart, replace the whole return statement below // TODO: Add Listview Future Builder 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 restaurants = snapshot.data?.restaurants ?? [];
final categories = snapshot.data?.categories ?? [];
final posts = snapshot.data?.friendPosts ?? [];
// TODO: Replace this with Restaurant Section
return const Center(
child: SizedBox(
child: Text('Show RestaurantSection'),
),
);
} else {
// 6
return const Center(
child: CircularProgressIndicator(),
);
}
},
);
Here’s what the code does:
-
FutureBuilderis a widget that works with asynchronous operations, allowing you to build UI based on the latest snapshot of aFuture. -
FutureBuildertakes in a future. You’re usinggetExploreData()to fetch data, which returns an instance ofExploreData. -
builder()is a function that decides what the UI should look like based on the current state of theFuture. This is provided by thesnapshot. - If
snapshot.connectionStateisdone, it means the data is available to consume. - Extract the data from
snapshot.data, providing default values if the data is null. For now, the widgets return a placeholder, you will replace it with actual content later. - If the data is not ready to consume, show a loading spinner.
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 Restaurant Section
The first scrollable component you’ll build is RestaurantSection. This is the top section of the ExplorePage. It will be a horizontal list view.
In lib/components, create a new file called restaurant_section.dart. Add the following code:
import 'package:flutter/material.dart';
// 1
import '../components/restaurant_landscape_card.dart';
import '../models/restaurant.dart';
class RestaurantSection extends StatelessWidget {
// 2
final List<Restaurant> restaurants;
const RestaurantSection({
super.key,
required this.restaurants,
});
@override
Widget build(BuildContext context) {
// 3
return Padding(
padding: const EdgeInsets.all(8.0),
// 4
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(left: 16.0, bottom: 8.0),
// 5
child: Text(
'Food near me',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
// TODO: Add Restaurant List View
// 6
Container(
height: 400,
// TODO: Add ListView Here
color: Colors.grey,
),
],
),
);
}
}
Here’s how the code works:
- Import restaurant card component and model.
-
RestaurantSectionis aStatelessWidgetthat requires a list of restaurants. - 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 “Food near me” section. - Add a
Container, 400 pixels tall, and set the background color togrey. This container is a placeholder for yourListViewof restaurants.
Adding the Restaurant Section
Open explore_page.dart and add the following import:
import '../components/restaurant_section.dart';
This means you don’t have to call additional imports when you use the new component.
Replace // TODO: Replace this with Restaurant Section and the return statement below with the following:
// TODO: Wrap in a ListView
return RestaurantSection(restaurants: restaurants);
If your app is still running, it will now look like this:
Now it’s finally time to add the ListView.
In restaurant_section.dart, replace // TODO: Add Restaurant List View and the Container beneath it with the following:
// 1
SizedBox(
height: 230,
// 2
child: ListView.builder(
// 3
scrollDirection: Axis.horizontal,
// 4
itemCount: restaurants.length,
// 5
itemBuilder: (context, index) {
// 6
return SizedBox(
width: 300,
// 7
child: RestaurantLandscapeCard(
restaurant: restaurants[index],
),
);
},
),
),
Here’s how the code works:
- The
ListViewwill have a fixed height of230pixels. It acts as a container to constraint the height of the child. -
ListView.builderwidget dynamically creates a list of items based on the provided data. - Configure the items in the
ListViewto scroll horizontally. - Set the
itemCountto be the length ofrestaurantslist. This determines how many items the list should render. -
itemBuilderis a function that returns a widget for a given index of the list. It’s invoked for each item in therestaurantlist. - Set a fixed width of
300pixels for every restaurant card. - Create a
RestaurantLandscapeCardwidget and pass in therestaurantobject based on the currentindex.
Add the following import:
import 'restaurant_landscape_card.dart';
Save the changes to trigger a hot restart and Yummy will now look like this; don’t forget, you can switch between light and dark mode:
You can scroll through the list of delicious restaurants. Finally!
Next, you’ll continue to add two new sections to ExplorePage.
Nested ListViews
There are two approaches to adding the category and post sections: the Column approach and the nested ListView approach. You’ll take a look at each of them now.
Column Approach
You could put the list views in a Column, that 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 of this approach are:
-
RestaurantSectionandCategorySectionare OK because the scroll direction is horizontal. All the cards also fit on the screen and everything looks great! -
PostSectionscrolls in the vertical direction, but it only has a small scroll area. So as a user, you can’t see 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.
ExplorePage holds the parent ListView. Since there are only three children 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
RestaurantSectionorCategorySectionin the horizontal direction. - When you scroll upward, Flutter listens to the scroll event of the parent
ListView. So it will scroll bothRestaurantSection,CategorySectionandPostSectionupwards, giving you more room to view all the content!
Nested ListView sounds like a better approach, doesn’t it?
Adding a Nested ListView
First, go back to explore_page.dart locate the comment // TODO: Wrap in a ListView and replace it and return RestaurantSection widget with the following:
// 1
return ListView(
// 2
shrinkWrap: true,
// 3
scrollDirection: Axis.vertical,
// 4
children: [
RestaurantSection(restaurants: restaurants),
// TODO: Add CategorySection
Container(
height: 300,
color: Colors.green,
),
// TODO: Add PostSection
Container(
height: 300,
color: Colors.orange,
),
],
);
Here’s how the code works:
- Initialize a scrollable list of widgets.
-
shrinkWrapsizes theListViewbased on its children’s height. - The list scrolls vertically.
- The list contains three child list view widgets. You will replace the two placeholder containers later.
Your app now looks like this, try scrolling up and down:
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 the CategorySection.
Building Category Section
The second scrollable component you’ll build is CategorySection. Users will be able to scroll through a list of food categories horizontally.
In lib/components, create a new file called category_section.dart. Add the following code:
import 'package:flutter/material.dart';
import '../models/food_category.dart';
import 'category_card.dart';
// 1
class CategorySection extends StatelessWidget {
final List<FoodCategory> categories;
const CategorySection({super.key, required this.categories});
@override
Widget build(BuildContext context) {
// 2
return Padding(
padding: const EdgeInsets.all(8.0),
// 3
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 4
const Padding(
padding: EdgeInsets.only(left: 16.0, bottom: 8.0),
child: Text(
'Categories',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
// 5
SizedBox(
height: 275,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: categories.length,
itemBuilder: (context, index) {
// 6
return SizedBox(
width: 200,
child: CategoryCard(
category: categories[index],
),
);
},
),
),
],
),
);
}
}
Here’s how the code works:
-
CategorySectionis aStatelessWidgetand requires a list of categories. The purpose of this widget is to display a list of various food categories. - The entire widget is wrapped by
Paddingwidget to ensure 8.0 pixel space all around. - The
Columnwidget is used to arrange child widgets vertically. - At the top of the column there is a title that displays “Categories”.
- After the title there is a horizontally-scrolling
ListView.builderwhich displays a list ofCategoryCardwidgets, each with a height of 200 pixels.
Now that you have created your category section, it’s time to add it to the list view.
Adding Category Section
Returning to explore_page.dart, locate // TODO: Add CategorySection and replace it and the Container below it with the following:
CategorySection(categories: categories),
Add the following import at the top:
import '../components/category_section.dart';
Your app now looks like this:
Next, you’ll replace the orange placeholder container with a PostSection.
Building the Post Section
The third scrollable component you’ll build is PostSection. Users will be able to scroll through a list of friend posts vertically.
In lib/components, create a new file called post_section.dart. Add the following code, ignoring any red squiggles:
import 'package:flutter/material.dart';
import '../models/post.dart';
// 1
class PostSection extends StatelessWidget {
final List<Post> posts;
const PostSection({
super.key,
required this.posts,
});
@override
Widget build(BuildContext context) {
// 2
return Padding(
padding: const EdgeInsets.all(8.0),
// 3
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(left: 16.0, bottom: 8.0),
// 4
child: Text(
'Friend\'s Activity',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
// 5
// TODO: Add Post List View
],
),
);
}
}
Here’s how the code works:
-
PostSectionis a stateless widget and requires a list ofPosts. - Apply overall padding of 8.0 pixels.
- Create a
Columnto position theTextfollowed by the posts in a vertical layout. - Create the
Textwidget header. - Use a placeholder comment to add the list of posts.
Next, locate the comment // TODO: Add Post List View and replace it with the following code:
// 1
ListView.separated(
// 2
primary: false,
// 3
shrinkWrap: true,
// 4
scrollDirection: Axis.vertical,
// 5
physics: const NeverScrollableScrollPhysics(),
itemCount: posts.length,
// 6
itemBuilder: (context, index) {
return PostCard(post: posts[index]);
},
separatorBuilder: (context, index) {
// 7
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
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. - Make this list view vertically scrollable.
- 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. - For every item in the list, create a
Postwidget. - For every item, also create a
SizedBoxto space each item by 16 pixels.
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.
If it didn’t automatically happen, add the following import at the top:
import 'post_card.dart';
The squiggles should be gone now. Next, you’ll add the code to show your friends’ posts.
Adding Post Section
Go back to explore_page.dart and find // TODO: Add PostSection and replace it and Container with the following:
PostSection(posts: posts),
If it’s not there, add the following import:
import '../components/post_section.dart';
Restart or hot reload the app. The final Explore page 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? :]
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:
-
CustomScrollView: A widget that creates custom scroll effects using slivers. Ever wonder how to collapse your navigation header on scroll? Use
CustomScrollViewfor 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.
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!
Where to Go From Here?
At this point, you’ve learned how to create ListViews. 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.