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

Section V: Deployment

Section 5: 4 chapters
Show chapters Hide chapters

5. Scrollable Widgets
Written by Vincent Ngo

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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:

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.

Models folder

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

Components folder

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

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

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.

Constructors

A ListView has four constructors:

Create ExploreScreen

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

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"));
  }
}

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'),

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)
];
import 'screens/explore_screen.dart';

Creating a FutureBuilder

How do you display your UI with an asynchronous task?

// 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());
      }
    });

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!

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

Add TodayRecipeListView

Open components.dart and add the following export:

export 'today_recipe_list_view.dart';
return TodayRecipeListView(recipes: recipes);

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);
})
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");
  }
}

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?

Nested ListView Approach

In the second approach, you nest multiple list views in a parent list view.

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());
      }
    }
  );
}

Creating FriendPostTile

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

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))
                    ])))
      ]);
  }
}
export 'friend_post_tile.dart';

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

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.

Key parameters

Here are some parameters you should pay attention to:

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!

Grid delegates

Grid delegates help figure out the spacing and the number of columns to use to layout the children to a GridView.

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());
          }
        });
  }
}
static List<Widget> pages = <Widget>[
  ExploreScreen(),
  RecipesScreen(),
  Container(color: Colors.blue)
];
import 'screens/recipes_screen.dart';

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:

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)
        ]
      )
    );
  }
}
export 'recipe_thumbnail.dart';

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);
            }));
  }
}
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);

Other Scrollable Widgets

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

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?

Solution

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

ScrollController _controller;
_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!");
  }
}
@override
void initState() {
  // 1
  _controller = ScrollController();
  // 2
  _controller.addListener(_scrollListener);
  super.initState();
}
return ListView(
        controller: _controller,
        ...

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),

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!

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now