Chapters

Hide chapters

Flutter Apprentice

First Edition - Early Access 2 · Flutter 1.20 · Dart 2.9 · AS 4.0.1

Section III: Navigating Between Screens

Section 3: 3 chapters
Show chapters Hide chapters

5. Scrollable Widgets
Written by Vincent Ngo

Building scrollable content is an essential part UI development. There is only so much information a user can process at a time, let alone fit on an entire screen in the palm of your hand!

In this chapter you will learn everything about scrollable widgets. In particular you will learn:

  • How to use ListView
  • How to nest scroll views
  • How to leverage the power of GridView

You will continue to build upon Fooderlich and you’ll build two new screens: Explore and Recipes. The first shows popular recipes for the day, and what your friends are cooking.

The second displays a library of recipes, handy if you are still on the fence about what to cook today :]

By the end of this chapter you will be a scrollable widget wizard!

Try saying Scrollable Widget Wizard fast, many times :]

Getting started

Open the starter project in Android Studio, run flutter pub get if necessary, then run the app.

You should see the Fooderlich app from the previous chapter:

Project files

Before you learn how to create scrollable widgets, there are new files in this starter project to help you out!

Assets folder

The assets directory contains all JSON files and images that you will use to build your app.

Sample images

  • food_pics contains all the food pictures you will display throughout the app.
  • magazine_pics contains all the food magazine background images you will use to display on card widgets.
  • profile_pics contains raywenderlich.com team member pictures.

JSON Data

The sample_data directory contains three JSON files:

  • sample_explore_recipes.json is a list of exploration recipes to display in the home screen. Sometimes users may like recommendations for what to cook today!
  • sample_friends_feed.json contains a sample list of friends’ posts. You might be curious about what your friends are cooking up! 👩‍🍳
  • sample_recipes.json is a list of recipes and has details about the duration and cooking difficulty of each.

New classes

In the lib directory, you will also notice three new folders as shown below:

API folder

The api folder contains a mock service class.

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.

There are two API calls you will use:

  • getExploreData() returns an ExploreData object. Internally, it makes a batch request and returns two lists: recipes to explore and friend posts.
  • getRecipes() returns a list of recipes.

Note: Unfamiliar with how async works in Dart? You can 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

There are six model objects you will use to build your app’s UI:

  • ExploreRecipe describes a recipe in great detail. It contains ingredients, instructions, duration and a whole lot more.
  • Ingredient describes a single ingredient. This is part of ExploreRecipe.
  • Instruction describes a single instruction to cook the recipe and 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 together. It contains a list of ExploreRecipes and a list of Posts.
  • SimpleRecipe describes 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

All the custom widgets are organized into the lib/components folder.

Note: components.dart is another barrel file that groups all imports in a single file.

Open home.dart and check out the pages property. Every single Card widget now requires an ExploreRecipe instance.

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")),
];

That’s it for getting up to speed on the new starter project files!

Now that you have a mock service and model objects out of the way, 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: Column and Row widgets are like ListView but without the scroll view.

Constructors

A ListView has four constructors:

  • The default constructor takes an explicit children list of widgets. That will construct every single child in the list, even the ones that are not 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 on screen. You should use this if you need to display a large or infinite number of items.
  • ListView.separated() takes two IndexedWidgetBuilder, itemBuilder and seperatorBuilder. This is very useful if you want to have a separator widget between your items.
  • ListView.custom() is for more fine grain control over your child items.

Check here for more details: https://api.flutter.dev/flutter/widgets/ListView-class.html

You will learn how to use the first three constructors!

Create ExploreScreen

The first screen you will create is the ExploreScreen. It contains two sections.

  • TodayRecipeListView is a horizontal scroll view that lets you pan through different cards.
  • FriendPostListView is 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();

  @override
  Widget build(BuildContext context) {
    // 2
    // TODO 1: Add TodayRecipeListView FutureBuilder
    return Center(
      child: Text("Explore Screen"));
  }
}

Here’s how the code works:

  1. Create a MockFooderlichService, to mock server responses.
  2. Display a placeholder text. You will replace this later.

Setup bottom navigation bar

Open home.dart and replace BottomNavigationBar’s items with the following:

BottomNavigationBarItem(icon: Icon(Icons.explore), label: 'Explore'),
BottomNavigationBarItem(icon: Icon(Icons.book), label: 'Recipes'),
BottomNavigationBarItem(icon: Icon(Icons.list), label: 'To Buy'),

You are just updating the icons and the labels of each BottomNavigationBarItem.

Update the navigation pages

In home.dart replace the pages property with the following:

static List<Widget> pages = <Widget>[
  ExploreScreen(),
  // TODO: Replace with RecipesScreen
  Container(color: Colors.green),
  Container(color: Colors.blue)
];

and make sure the new ExploreScreen class is imported. Add this import if it isn’t automatically added by your IDE.

import 'screens/explore_screen.dart';

Run the app now. It should currently look like this:

Perform a hot restart, or fully restart the app, if you don’t see the three tabs as above.

The first screen you will build is the ExploreScreen. You will replace the Container widgets 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 completed.

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
        var recipes = snapshot.data.todayRecipes;
        // TODO: Replace this with TodayRecipeListView
        return Center(
            child: Container(
                child: Text("Show TodayRecipeListView")));
      } else {
        // 6
        return Center(
            child: CircularProgressIndicator());
      }
    });

Here is what the code does:

  1. Within the widget’s build() function you create a FutureBuilder.
  2. The FutureBuilder takes in a Future as a parameter. getExploreData() creates a future that will in turn return an ExploreData instance. Such an instance will contain two lists, todayRecipes and friendPosts.
  3. Within the builder function you use snapshot to check the current state of the Future.
  4. The Future is complete and you can extract the data to pass to your widget.
  5. snapshot.data returns ExploreData, from which you extract todayRecipes to pass to the list view. Right now you show a simple text as placeholder. You will build a TodayRecipeListView soon.
  6. The future is still loading, so show a spinner to let the user know something is loading.

Note: For more information about FutureBuilder check out: https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html

Perform a hot reload. You should first see the loading spinner. After the future completes it shows the placeholder text.

Now that the loading UI is setup, it’s time to build the actual list view!

Building Recipes of the Day 🍳

The first scrollable component you will build is TodayRecipeListView. This is the top section of the ExploreScreen. This is going to be a horizontal list view!

In the lib/components folder 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: EdgeInsets.only(left: 16, right: 16, top: 16),
      // 4
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 5
          Text(
            "Recipes of the Day 🍳",
            style: Theme.of(context).textTheme.headline1),
          // 6
          SizedBox(height: 16),
          // 7
          Container(
            height: 400,
            // TODO: Add ListView Here
            color: Colors.grey,
          )
        ]
      )
    );
  }
}

Here’s how the code works:

  1. Import the barrel files component.dart and models.dart so you can use data models and UI components.
  2. TodayRecipeListView needs a list of recipes to display.
  3. Within the build() method, start by applying some padding.
  4. Add a Column widget to place widgets in a vertical layout.
  5. In the column, add a Text widget. This is the header for the recipes of the day.
  6. Add a 16 point tall SizedBox, to have some padding
  7. Add a Container widget, 400 points tall, and set the background color to grey. This container will hold your horizontal list view.

Add TodayRecipeListView

Open components.dart and add the following export:

export 'today_recipe_list_view.dart';

This is so you don’t have to call additional imports when using 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 should 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
    var recipe = recipes[index];
    return buildCard(recipe);
  },
  // 7
  separatorBuilder: (context, index) {
    // 8
    return SizedBox(width: 16);
})

and make sure to delete the existing color property of the container.

Here’s how the code works:

  1. Change the color from grey to transparent.
  2. Create a ListView.separated widget. Recall this widget creates two IndexedWidgetBuilder.
  3. Set the scroll direction to the horizontal axis.
  4. Set the number of items in the list view.
  5. Create the itemBuilder callback that will go through every item in the list.
  6. Get the recipe for the current index, and build the card widget.
  7. Create the separatorBuilder callback that will go through every item in the list.
  8. For every item, you create a SizedBox widget to space every item 16 points apart.

Just below the build() method add the following code:

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 object has a cardType property. This helps you determine which Card widget to create for that recipe.

You need to add imports for the card classes. You can do that by selecting them with the mouse and hitting option+return on Mac or Alt+Enter on PC. Then chose the correct import to add.

When you restart, the Fooderlich app should now look like this:

You can scroll throught the list of beautiful recipes for the day. Don’t forget you can switch the theme in main.dart to dark mode!

Next, you are going to build the bottom section of ExploreScreen.

Nested ListViews

There are two approaches to building the bottom section.

Column Approach

You could put the two list views in a Column widget. A Column widgets arranges items in a vertical layout. This makes sense right?

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 screen and look great!
  • FriendPostListView widget 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 one time.

This approach has a bad user experience because the content area is too small! The Card widgets 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.

The ExploreScreen holds the parent ListView widget. Since there are only two child ListView widgets, you can make use of the default constructor that returns an explicit list of children.

The benefit of this approach:

  1. The scroll area is a lot bigger taking 70-80% of the screen.
  2. You can view more of the friend posts.
  3. You can continue to scroll TodayRecipeListView in the horizontal direction.
  4. When you scroll upward, Flutter actually listens to the scroll event of the parent ListView! So it will scroll both TodayRecipeListView and FriendPostListView upwards! More room to view all the content!

Nested ListView sounds like a better approach, doesn’t it?

Adding Nested ListView

First open explore_screen.dart and replace the build() method with the following:

@override
Widget build(BuildContext context) {
  // 1
  return FutureBuilder(
    // 2
    future: mockService.getExploreData(),
    // 3
    builder: (context, snapshot) {
      // 4
      if (snapshot.connectionState == ConnectionState.done) {
        // 5
        return ListView(
          // 6
          scrollDirection: Axis.vertical,
          children: [
            // 7
            TodayRecipeListView(recipes: snapshot.data.todayRecipes),
            // 8
            SizedBox(height: 16),
            // 9
            // TODO: Replace this with FriendPostListView
            Container(height: 400, color: Colors.green)
          ]
        );
      } else {
        // 10
        return Center(child: CircularProgressIndicator());
      }
    }
  );
}

Here’s how the code works:

  1. This is the FutureBuilder from before. It runs an asynchronous task and lets you know the state of the future.
  2. Use your mock service to call getExploreData(). This returns an ExploreData object future.
  3. Check the state of the future within the builder callback
  4. Check if the future is complete.
  5. When the future is complete, return the primary ListView. This holds an explicit list of children. In this scenario, the primary ListView will hold the other two ListView as children.
  6. Set the scroll direction to vertical, although that’s the default value
  7. The first item in the children list is TodayRecipeListView. You pass in the list of todayRecipes from your ExploreData object.
  8. Add a 16 point vertical space so that lists are not too close to each other.
  9. Add a placeholder green container. You will create and add the FriendPostListView later.
  10. If the future has not finished loading yet, show a circular progress indicator.

Your app should now look like this:

Notice that you can still scroll the Cards horizontally. When you scroll up and down you will notice the entire area scrolls!

Now that you have the desired scroll behavior, it’s time to build the FriendPostListView!

Creating FriendPostTile

First, you’ll create the items for the list view to display. Below is the FriendPostTile widget you will create:

Within the lib/components directory, 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(AssetImage(post.profileImageUrl),
            imageRadius: 20),
        // 3
        SizedBox(width: 16),
        // 4
        Expanded(
            child: Container(
                // 5
                child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      // 6
                      Text(post.comment),
                      // 7
                      Text("${post.timestamp} mins ago",
                          style: TextStyle(fontWeight: FontWeight.w700))
                    ])))
      ]);
  }
}

Here’s how the code works:

  1. Create a Row to arrange widgets horizontally.
  2. The first element is a circular avatar, displaying the image asset associated with the post.
  3. Apply a 16 point padding.
  4. Create an Expanded widget, so that children fill the rest of the container.
  5. Establish a Column widget to arrange widgets vertically.
  6. Create a Text widget to display a friend’s comment.
  7. Create another Text widget to display the timestamp of a post.

Note there is no height restriction on the FriendPostTile widget. That means the text can expand to many lines as long as it is in a scroll view! This is like iOS’s dynamic table views and auto sizing text views 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 the lib/components directory create a new file called friend_post_list_view.dart and add the following code:

import 'package:flutter/material.dart';
import '../models/models.dart';
import 'components.dart';

class FriendPostListView extends StatelessWidget {
  // 1
  final List<Post> friendPosts;

  const FriendPostListView({Key key, this.friendPosts}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 2
    return Padding(
        padding: EdgeInsets.only(left: 16, right: 16, top: 0),
        // 3
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 4
            Text(
              "Social Chefs 👩‍🍳",
              style: Theme.of(context).textTheme.headline1),
            // 5
            SizedBox(height: 16),
            // TODO: Add PostListView here
            // 6
            SizedBox(height: 16),
        ]));
  }
}

Here’s how the code works:

  1. FriendPostListView requires a list of Post objects.
  2. Apply a left and right padding widget of 16 points.
  3. Create a Column widget to layout the Text widget followed by the posts in a vertical layout.
  4. Create the Text widget header.
  5. Apply a spacing of 16 points vertically.
  6. 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: NeverScrollableScrollPhysics(),
  // 4
  shrinkWrap: true,
  scrollDirection: Axis.vertical,
  itemCount: friendPosts.length,
  itemBuilder: (context, index) {
    // 5
    var post = friendPosts[index];
    return FriendPostTile(post: post);
  },
  separatorBuilder: (context, index) {
    // 6
    return SizedBox(height: 16);
  }),

Here’s how the new ListView is defined:

  1. Create a ListView.seperated. This will have two IndexWidgetBuilder callbacks.
  2. Since you are nesting two list views, it is a good idea to set primary to false. That will let Flutter know that this is not the primary scroll view.
  3. 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.
  4. Set shrinkWrap to true to create a fixed-length scrollable list of items. This means it has a fixed height! If this is false you would get an unbounded height error.
  5. For every item in the list, create a FriendPostTile.
  6. For every item also create a SizedBox to space each item by 16 points.

Note There are actually different type of scroll physics you can play with:

  • AlwaysScrollableScrollPhysics
  • BouncingScrollPhysics
  • ClampingScrollPhysics
  • FixedExtentScrollPhysics
  • NeverScrollableScrollPhysics
  • PageScrollPhysicsRange
  • MaintainingScrollPhysics

More details are in the Flutter documentation at https://api.flutter.dev/flutter/widgets/ScrollPhysics-class.html.

Open components.dart and add the following export:

export 'friend_post_list_view.dart';

Final touches for ExploreScreen

Open explore_screen.dart and replace the code below the comment // TODO: Replace this with FriendPostListView with the following:

FriendPostListView(friendPosts: snapshot.data.friendPosts)

Here you create a FriendPostListView and extract friendPosts from the ExploreData object.

Restart or hot reload the app. The final Explore screen should look like the following in light mode:

Here is what it looks like in dark mode:

Aren’t nested scroll views a neat technique? :]

Now it’s time to play with grid views.

GridView

GridView is a 2D array of scrollable widgets. It arranges the children in a grid and supports horizontal and vertical scrolling.

Constructors

Getting used to GridView is easy. Like ListView, it inherits from ScrollView, so their constructors are very similar.

GridView has five types of constructors:

  • The default is similar to ListView and takes an explicit list of widgets.
  • GridView.builder()
  • GridView.count()
  • GridView.custom()
  • GridView.extent()

The builder() and count() constructors are the most common. You will have no problem getting use to these since you did something similar with ListView.

Key parameters

Here are some parameters you should pay attention to:

  • crossAxisSpacing is the spacing between each child in the cross axis.
  • mainAxisSpacing is the spacing between each child on the main axis.
  • crossAxisCount is 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 scroll area fixed size.
  • physics controls how the scroll view should respond to user input.
  • primary helps Flutter determine which scroll view is the primary scroll view.
  • 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

What’s cross and main axis?

You may be wondering what is the difference between main axis and cross axis! Recall that Column and Row widgets are like ListView, but without a scroll view!

Note: The main axis will always correspond to the scroll direction!

If your scroll direction is horizontal you can think of this like a Row widget. The main axis represents the horizontal direction. As shown below:

If your scroll direction is vertical you can think of this like a Column widget. 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 layout the children to 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.

Recipes Screen

You are now ready to build the recipes screen! Within the screens directory create a new file called recipes_screen.dart. Add the following code:

import 'package:flutter/material.dart';
import '../api/mock_fooderlich_service.dart';
import '../components/components.dart';

class RecipesScreen extends StatelessWidget {
  // 1
  final exploreService = MockFooderlichService();

  @override
  Widget build(BuildContext context) {
    // 2
    return FutureBuilder(
        // 3
        future: exploreService.getRecipes(),
        builder: (context, snapshot) {
          // 4
          if (snapshot.connectionState == ConnectionState.done) {
            // TODO: Add RecipesGridView Here
            // 5
            return Center(child: Text("Recipes Screen"));
          } else {
            // 6
            return Center(child: CircularProgressIndicator());
          }
        });
  }
}

The code is a very similar setup to ExploreScreen:

  1. Create a mock service.
  2. Create a FutureBuilder widget.
  3. getRecipes() returns the list of recipes to display. This function returns a future list of SimpleRecipe objects.
  4. Check if the future is complete.
  5. Add a placeholder text until you build RecipesGridView.
  6. Show a circular loading indicator if the future is not yet complete.

In home.dart replace the pages property with:

static List<Widget> pages = <Widget>[
  ExploreScreen(),
  RecipesScreen(),
  Container(color: Colors.blue)
];

Next, add the following import:

import 'screens/recipes_screen.dart';

Build 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 is the thumbnail widget you will create:

It’s a simple tile that displays the picture, the name and the duration of a recipe!

Within the lib/components directory 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: EdgeInsets.all(8),
      // 3
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 4
          Expanded(
              child: Container(
                  // 5
                  child: ClipRRect(
                      child: Image.asset("${recipe.dishImage}",
                          fit: BoxFit.cover),
                      borderRadius: BorderRadius.circular(12)))),
          // 6
          SizedBox(height: 10),
          // 7
          Text(
              recipe.title,
              maxLines: 1,
              style: Theme.of(context).textTheme.bodyText1),
          Text(
              recipe.duration,
              style: Theme.of(context).textTheme.bodyText1)
        ]
      )
    );
  }
}

Here’s how the code works:

  1. This class requires a SimpleRecipe object as a parameter. That will help to configure your widget.
  2. Create a Container with 8 points padding all around.
  3. Use a Column widget to apply a vertical layout.
  4. The first element of the column is an Expanded widget. That widget holds on to a Container widget, which will then hold on to your Image widget. You want the image to fill the remaining space!
  5. The Image widget is within the ClipRRect widget, which helps to clip the image to make the borders rounded.
  6. Make some room between the image and the other widgets.
  7. Add the remaining Text widgets: 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 are ready to create your grid view!

Creating RecipesGridView

Within the lib/components directory create a new file called recipes_grid_view.dart and add the following code:

import 'package:flutter/material.dart';
import '../components/components.dart';
import '../models/models.dart';

class RecipesGridView extends StatelessWidget {
  // 1
  final List<SimpleRecipe> recipes;

  const RecipesGridView({Key key, this.recipes}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 2
    return Padding(
        padding: EdgeInsets.only(left: 16, right: 16, top: 16),
        // 3
        child: GridView.builder(
            // 4
            itemCount: recipes.length,
            // 5
            gridDelegate:
                SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
            itemBuilder: (context, index) {
              // 6
              var simpleRecipe = recipes[index];
              return RecipeThumbnail(recipe: simpleRecipe);
            }));
  }
}

A GridView is similar to a ListView. Here is how it works:

  1. RecipesGridView requires a list of recipes to display in a grid.
  2. Apply a 16 point padding on the left, right, and top.
  3. Create a GridView.builder, which displays only the items visible on screen.
  4. Tell the grid view how many items will be in the grid.
  5. Add SliverGridDelegateWithFixedCrossAxisCount and set the crossAxisCount to 2. That means that there will be only two columns.
  6. 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 up recipes_screen.dart and replace the return statement below the comment // TODO: Add RecipesGridView Here with the following:

return RecipesGridView(recipes: snapshot.data);

Your RecipesScreen is all setup!

If you still have your app running, then perform a hot reload, and the new screen should look like this:

Other Scrollable Widgets

There are many more scrollable widgets for various different use cases. Here are some not covered in this chapter:

  • PageView is a scrollable widget that scrolls page by page. This is commonly used for an onboarding flow. It also supports a vertical scroll direction as well!

  • CustomScrollView is 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 is 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 for this.

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:

  1. print("i am at the bottom!"), if the user scrolls to the bottom.
  2. 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 is a step-by-step hint:

  1. Make ExploreScreen a stateful widget.
  2. Create an instance of ScrollController in the initState().
  3. Create a scrollListener() function to listen to scroll position.
  4. Add a scroll listener to the scroll controller.
  5. Add the scroll controller to the ListView.

Solution

First you need to make ExploreScreen a StatefulWidget. That is because you need to preserve the state of the scroll controller.

Next add a ScrollController property in _ExploreScreenState:

ScrollController _controller;

Then, add a function called scrollListener(), which is the function callback that will listen to the scroll offsets.

_scrollListener() {
  // 1
  if (_controller.offset >= _controller.position.maxScrollExtent &&
      !_controller.position.outOfRange) {
    print("i am at the bottom!");
  }
  // 2
  if (_controller.offset <= _controller.position.minScrollExtent &&
      !_controller.position.outOfRange) {
    print("i am at the top!");
  }
}

Here’s how the code works:

  1. Check the scroll offset, and see if the position is greater than or equal to the maxScrollExtent. That means the user has scrolled to the very bottom.
  2. Check if the scroll offset is less than or equal to the minScrollExtend. That means the user has scrolled to the very top.

Within _ExploreScreenState, override the initState() method as shown below:

@override
void initState() {
  // 1
  _controller = ScrollController();
  // 2
  _controller.addListener(_scrollListener);
  super.initState();
}

Here’s how the code works:

  1. You initialize the scroll controller.
  2. You add a listener to the controller. Every time the user scrolls, scrollListener() will get called.

Within the ExploreScreen’s parent ListView, all you have to do is set the scroll controller as shown below:

return ListView(
        controller: _controller,
        ...

That will tell the scroll controller to listen to this particular list view’s scroll events.

Some use cases for when you might need a scroll controller:

  • Detect if you are at a certain offset.
  • Control the scroll movement by animating to a specific index.
  • Check to see if the scroll view has started, stop, or ended.

Challenge 2: New GridView Layout

Try using SliverGridDelegateWithMaxCrossAxisExtent to create the grid layout below, which displays recipes only in one column:

Solution

In recipes_grid_view.dart, replace the gridDelegate parameter with the following:

SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 500),

Recall that the GridView is set to scroll in the vertical direction. That means the cross axis is horizontal. According to Flutter’s documentation, maxCrossAxisExtent sets the maximum extent of tiles in the cross axis. So making maxCrossAxisExtent greater than the device’s width would allow for only one column!

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.
  • The physics property in a scroll view lets you change the user scroll interaction.
  • Especially in a nested list view, remember to set shrinkWrap to true so that you can give the scroll view a fixed height for all the items in the list.
  • Use a FutureBuilder to wait for an async task to complete.
  • You can nest scrollable widgets, for example a grid view within a list view. Unleash your wildest imagination!
  • Use ScrollController and ScrollNotification to control or listen to scroll behavior.
  • Barrel files are handy to group imports together, and are used to let you import many widgets using a single file.

Where to go from here?

You have learned how to create 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 now build some cool scroll interactions!

You are ready to look like a pro in front of your friends :]

For more examples check out the Flutter web gallery, 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.