Chapters

Hide chapters

Flutter Apprentice

Fourth Edition · Flutter 3.16.9 · Dart 3.2.6 · Android Studio 2023.1.1

Section II: Everything’s a Widget

Section 2: 5 chapters
Show chapters Hide chapters

Section IV: Networking, Persistence & State

Section 4: 6 chapters
Show chapters Hide chapters

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.

YummyService YummyAPI MockYummyService

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 async works 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: Column and Row widgets are like ListView 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 an IndexedWidgetBuilder 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 two IndexedWidgetBuilders: itemBuilder and seperatorBuilder. 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.

CategorySection RestaurantSection PostSection

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:

  1. Create a MockYummyService, to mock server responses.
  2. 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:

  1. FutureBuilder is a widget that works with asynchronous operations, allowing you to build UI based on the latest snapshot of a Future.
  2. FutureBuilder takes in a future. You’re using getExploreData() to fetch data, which returns an instance of ExploreData.
  3. builder() is a function that decides what the UI should look like based on the current state of the Future. This is provided by the snapshot.
  4. If snapshot.connectionState is done, it means the data is available to consume.
  5. 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.
  6. If the data is not ready to consume, show a loading spinner.

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 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:

  1. Import restaurant card component and model.
  2. RestaurantSection is a StatelessWidget that requires a list of restaurants.
  3. Within build(), start by applying some padding.
  4. Add a Column to place widgets in a vertical layout.
  5. In the column, add a Text. This is the header for the “Food near me” section.
  6. Add a Container, 400 pixels tall, and set the background color to grey. This container is a placeholder for your ListView of 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:

  1. The ListView will have a fixed height of 230 pixels. It acts as a container to constraint the height of the child.
  2. ListView.builder widget dynamically creates a list of items based on the provided data.
  3. Configure the items in the ListView to scroll horizontally.
  4. Set the itemCount to be the length of restaurants list. This determines how many items the list should render.
  5. itemBuilder is a function that returns a widget for a given index of the list. It’s invoked for each item in the restaurant list.
  6. Set a fixed width of 300 pixels for every restaurant card.
  7. Create a RestaurantLandscapeCard widget and pass in the restaurant object based on the current index.

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.

Yummy Column RestaurantSection PostSection CategorySection

The pros and cons of this approach are:

  • RestaurantSection and CategorySection are OK because the scroll direction is horizontal. All the cards also fit on the screen and everything looks great!
  • PostSection scrolls 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.

Yummy RestaurantSection CategorySection PostSection ExplorePage ListView

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:

  1. The scroll area is a lot bigger, using 70–80% of the screen.
  2. You can view more of your friends’ posts.
  3. You can continue to scroll RestaurantSection or CategorySection in the horizontal direction.
  4. When you scroll upward, Flutter listens to the scroll event of the parent ListView. So it will scroll both RestaurantSection, CategorySection and PostSection upwards, 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:

  1. Initialize a scrollable list of widgets.
  2. shrinkWrap sizes the ListView based on its children’s height.
  3. The list scrolls vertically.
  4. 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:

  1. CategorySection is a StatelessWidget and requires a list of categories. The purpose of this widget is to display a list of various food categories.
  2. The entire widget is wrapped by Padding widget to ensure 8.0 pixel space all around.
  3. The Column widget is used to arrange child widgets vertically.
  4. At the top of the column there is a title that displays “Categories”.
  5. After the title there is a horizontally-scrolling ListView.builder which displays a list of CategoryCard widgets, 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:

  1. PostSection is a stateless widget and requires a list of Posts.
  2. Apply overall padding of 8.0 pixels.
  3. Create a Column to position the Text followed by the posts in a vertical layout.
  4. Create the Text widget header.
  5. 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:

  1. Create ListView.separated with two IndexWidgetBuilder callbacks.
  2. 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.
  3. Set shrinkWrap to true 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.
  4. Make this list view vertically scrollable.
  5. Set the scrolling physics to NeverScrollableScrollPhysics. Even though you set primary 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.
  6. For every item in the list, create a Post widget.
  7. For every item, also create a SizedBox to space each item by 16 pixels.

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.

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 CustomScrollView for more fine-grain control over your scrollable area!

Slivers Slivers

  • 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 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!

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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.