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

6. Advanced Scrollable Widgets
Written by Vincent Ngo

You’ve got the hang of scrollable widgets, but there’s so much more to explore. Don’t limit your app to just mobile screens—Flutter excels at adapting to various devices, from phones to tablets, desktops and the web. With Flutter, create an app that’s not only mobile-friendly but effortlessly scales to any screen size. Embrace versatility and go universal.

In this chapter, you’ll delve deeper into the world of scrollable widgets. You’ll learn how to:

  • Create custom scroll effects with the Sliver widget.
  • Make your UI responsive with the GridView widget.

You’ll continue to build out your food app, Yummy, by introducing a new feature: the RestaurantPage. Here, users can tap on a restaurant to explore everything from today’s menu to a gallery of enticing dishes, all displayed responsively.

Heads up: Grab a snack! The sight of food pictures might just work up an appetite.

Here is what the mobile view looks like:

And here’s the experience reimagined for the web:

In a bit your app will look and function beautifully on any device, providing a seamless and responsive experience from mobile devices to the web.

Getting Started

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

You’ll see the explore page as shown below:

Note In this chapter you’ll be running the app on mobile and web to test and develop responsive a UI. In Android Studio you can run multiple devices by clicking the drop-down menu as shown below:

New Files in the Project

There are new files in this starter project to help you out. Before you start take a look at them.

Open the new lib/components/restaurant_item.dart file. You’ll find the class RestaurantItem.

This widget is designed to showcase individual menu items in a restaurant’s menu.

Here’s a breakdown of its components:

  • Title
  • Description
  • Price
  • Popularity indicator
  • Image
  • Add button, to add an item to the cart

Introducing Slivers

Slivers in Flutter are a fundamental part of creating custom scroll effects in a scrollable area. They are a family of widgets that provide various ways to lay out a list of children in a scrolling view.

Unlike more straightforward widgets like ListView or GridView, slivers give developers fine-grained control over scroll behavior, animation, and the geometry of scrolling elements, making them the building blocks for complex scrollable areas.

Slivers Slivers

Types of Slivers

Slivers operate within the CustomScrollView widget, which allows them to combine different scrolling behaviors in a single scroll view.

  • SliverList and SliverGrid are the sliver equivalents of ListView and GridView, respectively. They allow you to lay out items linearly or in a grid pattern.
  • SliverAppBar is a highly flexible app bar that can expand, collapse, float, and snap as you scroll.
  • SliverToBoxAdapter allows you to place a single non-sliver widget within a CustomScrollView.
  • SliverFillRemaining and SliverFillViewport let you size children based on the remaining space in the viewport, creating dynamic effects as you scroll.

Note: For a deeper dive into how you can leverage slivers to create various scrolling effects, you can review Flutter’s documentation.

Ready to explore the world of slivers? Let’s get scrolling!

Building the Restaurant Page

First you need to set up the RestaurantPage. When users click on a restaurant in the Food Near Me section of your app it will direct them to this page that displays the restaurant’s menu.

In lib/screens, create a new file called restaurant_page.dart. Add the following code:

// 1
import 'package:flutter/material.dart';
import '../models/restaurant.dart';

// 2
class RestaurantPage extends StatefulWidget {
  final Restaurant restaurant;

  // 3
  const RestaurantPage({
    super.key,
    required this.restaurant,
  });

  @override
  State<RestaurantPage> createState() => _RestaurantPageState();
}

// 4
class _RestaurantPageState extends State<RestaurantPage> {
  // TODO: Add Desktop Threshold
  // TODO: Add Constraint Properties
  // TODO: Calculate Constrained Width
  // TODO: Add Calculate Column Count

  // TODO: Build Custom Scroll View
  // TODO: Build Sliver App Bar
  // TODO: Build Info Section
  // TODO: Build Grid Item
  // TODO: Build Section Title
  // TODO: Build Grid View
  // TODO: Build Grid View Section

  // TODO: Replace build method
  @override
  Widget build(BuildContext context) {
    // 5
    return Scaffold(
      body: Center(
        // TODO: Replace with Custom Scroll View
        child: Text(
          'Restaurant Page',
          style: TextStyle(fontSize: 16.0),),
      ),
    );
  }
}

Here’s how the code works:

  1. Import necessary packages and classes.
  2. Define a new class RestaurantPage as a StatefulWidget so we can have a mutable state.
  3. RestaurantPage takes in a restaurant object which contains data such as restaurant info and menu to display on the page.
  4. _RestaurantPageState is a class that holds the state for RestaurantPage.
  5. The build() method currently returns a temporary placeholder text that displays Restaurant Page. This is where you’ll add the new elements of this view.

Navigating to a Restaurant Page

Let’s first implement a way to display a single restaurant.

Open lib/components/restaurant_landscape_card.dart.

Locate // TODO: Push Restaurant Page and replace it with the following code:

// 1
Navigator.push(
  // 2
  context,
  // 3
  MaterialPageRoute(
    // 4
    builder: (context) =>
      RestaurantPage(restaurant: widget.restaurant,
    )
  ),
);

When the user taps on a restaurant it navigates to its RestaurantPage. Here’s how the code works:

  1. Navigator.push starts the navigation to a new screen.
  2. context tells Flutter where the navigation starts from within the widget tree.
  3. MaterialPageRoute is used to create a route with a standard transition animation.
  4. Navigate to RestaurantPage and pass in the current restaurant object to be displayed.

Add the following import:

import '../screens/restaurant_page.dart';

Perform a hot reload. In the Foods Near Me section tap on a restaurant and you should see the new RestaurantPage as shown below:

Now that your page is set up you’re now ready to construct your sliver.

Building a Sliver for the Restaurant Page

Return to lib/screens/restaurant_page.dart, replace // TODO: Build Custom Scroll View with the following:

CustomScrollView _buildCustomScrollView() {
  return CustomScrollView(
    slivers: [
      // TODO: Add Sliver App Bar
      SliverToBoxAdapter(
          child: Container(
              height: 200.0,
              color: Colors.red,),),
      // TODO: Add Restaurant Info Section
      SliverToBoxAdapter(
          child: Container(
              height: 300.0,
              color: Colors.green,),),
      // TODO: Add Menu Item Grid View Section
      SliverFillRemaining(
          child: Container(
              color: Colors.blue,),),
    ],
  );
}

This function returns a CustomScrollView, a versatile widget that coordinates a variety of sliver widgets to create a multifaceted scrolling interface. Initially, this scroll view is populated with placeholder slivers:

  • SliverToBoxAdapter is a handy widget that adapts a standard, non-sliver widget for use within a sliver list.
  • SliverFillRemaining sizes its children to occupy the available remaining space in the viewport, perfect for expanding content areas.

These placeholders will be replaced later with the actual content for the app’s scrolling layout.

In the same file, locate the comment // TODO: Replace with Custom Scroll View and replace it and child with the following:

child: _buildCustomScrollView(),

Perform a hot reload, and you should see the following screen(s):

Next, you’ll create a SliverAppBar that remains pinned to the top of a scrollable area.

Building a Sliver App Bar

A SliverAppBar expands to reveal a large image of a restaurant with a circular icon overlayed at the bottom left. As the user scrolls up, the app bar will remain at the top, and shrink the regular app bar size.

Locate // TODO: Build Sliver App Bar and replace it with the following code:

SliverAppBar _buildSliverAppBar() {
  // 1
  return SliverAppBar(
    // 2
    pinned: true,
    // 3
    expandedHeight: 300.0,
    // 4
    flexibleSpace: FlexibleSpaceBar(
      // 5
      background: Center(
        // 6
        child: Padding(
          padding: const EdgeInsets.only(
            left: 16.0,
            right: 16.0,
            top: 64.0,
          ),
          // 7
          child: Stack(
            children: [
              // 8
              Container(
                margin: const EdgeInsets.only(bottom: 30.0),
                decoration: BoxDecoration(
                  color: Colors.grey,
                  borderRadius: BorderRadius.circular(16.0),
                  // 9
                  image: DecorationImage(
                    image: AssetImage(widget.restaurant.imageUrl),
                    fit: BoxFit.cover,),),
                  ),
              // 10
              const Positioned(
                bottom:0.0,
                left: 16.0,
                child: CircleAvatar(
                  radius: 30,
                  child: Icon(Icons.store, color: Colors.white,),
                ),
              ),
            ],
          ),
        ),
      ),
    ),
  );
}

Here’s how the code works:

  1. The function returns a SliverAppBar widget that creates a collapsible app bar.
  2. Keep the app bar pinned at the top of the view.
  3. Specify expandedHeight of 300.0 pixels for maximum height when fully expanded.
  4. Use a FlexibleSpaceBar for the collapsible part of the app bar.
  5. Within the FlexibleSpaceBar, set a background widget.
  6. Apply some padding to create internal spacing for the background.
  7. Arrange elements using a Stack.
  8. Create a Container for the backdrop with styling.
  9. Show the restaurant image as a background using DecorationImage.
  10. Place a circular icon at the bottom left using Positioned widget.

Integrate the Sliver App Bar

Now add the app bar to your custom scroll view. Locate // TODO: Add Sliver App Bar and replace it and SliverToBoxAdapter with the following:

_buildSliverAppBar(),

Perform a hot reload. Tap on a restaurant, and you should see the app bar as shown below:

When you look at a restaurant knowing its details is helpful, but you don’t have that. No worries, you’ll add the info section next.

Building the Restaurant Info Section

After adding a sliver containing the restaurant’s information such as the name, address, rating, distance and attributes your app bar will look like this:

Locate the comment // TODO: Build Info Section and replace it with the following code:

// 1
SliverToBoxAdapter _buildInfoSection() {
  // 2
  final textTheme = Theme.of(context).textTheme;
  // 3
  final restaurant = widget.restaurant;
  // 4
  return SliverToBoxAdapter(
    // 5
    child: Padding(
      padding: const EdgeInsets.all(16.0),
      // 6
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 7
          Text(restaurant.name, style: textTheme.headlineLarge,),
          Text(restaurant.address, style: textTheme.bodySmall,),
          Text(
            restaurant.getRatingAndDistance(),
            style: textTheme.bodySmall,),
          Text(restaurant.attributes, style: textTheme.labelSmall,),
        ],
      ),
    ),
  );
}

Here’s how the code works:

  1. Create _buildInfoSection() to construct a UI section for the restaurant’s details.
  2. Retrieve the application’s text styles for consistent theming.
  3. Access the restaurant’s data passed to the widget.
  4. Create a SliverToBoxAdapter to enable a column of text widgets in a sliver-based layout.
  5. Apply padding around the column for spacing.
  6. Create a column and align text elements to the start of the column.
  7. Display the restaurant’s name, address, rating, and attributes with styled text widgets.

Replace // TODO: Add Restaurant Info Section and SliverToBoxAdapter beneath it with:

_buildInfoSection(),

Perform a hot reload. Tap on a restaurant, and you should see the app bar as shown below:

Next, you’ll use a grid view to display the collection of menu items for a restaurant. Before you do that, take a moment to get acquainted with the GridView widget.

Introducing GridView

GridView is a 2D array of scrollable widgets. Similar to its linear counterpart, the ListView, it supports both horizontal and vertical scrolling, but it arranges its children in a grid format, which is perfect for displaying multiple items in a clean, organized layout.

cross axis main axis

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

  • The default GridView.count() constructor is great for creating a grid with a fixed number of tiles in the cross-axis. You would use this if you know the number of columns or rows you want upfront.
  • GridView.builder() works similarly to ListView.builder() by lazily constructing items as they’re scrolled into the viewport, ideal for displaying a large or indeterminate number of items.
  • GridView.custom() offers the highest level of customization, letting you use a custom SliverGridDelegate for precise control over how your grid is laid out.
  • GridView.extent() allows you to specify the maximum extent of the tiles in the cross-axis, and it will determine the number of tiles in each row or column dynamically based on the available space.

GridView Key Parameters

Here are some parameters you should pay attention to:

  • crossAxisSpacing: The spacing between each child in the cross-axis.
  • mainAxisSpacing: The spacing between each child on the main axis.
  • crossAxisCount: The number of children in the cross-axis. You can also think of this as the number of columns you want in a grid.
  • shrinkWrap. Controls the fixed scroll area size.
  • physics: Controls how the scroll view responds to user input.
  • primary: Helps Flutter determine which scroll view is the primary one.
  • scrollDirection: Controls the axis along which the view will scroll.

Note 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.

Understanding the Cross and Main Axis

What’s the difference between the main and cross axis? Remember that Columns and Rows are like ListViews, but without a scroll view.

The main axis always corresponds to the scroll direction!

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

Row main axis cross axis

If your scroll direction is vertical, you can think of it as a Column. The main axis represents the vertical direction, as shown below:

main axis Column cross axis

Understanding Grid Delegates

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

Aside from customizing your 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.

Building the Grid View Section

You’ll use the GridView widget to display a collection of items on a restaurant’s menu. GridView is a great widget to make your app responsive on different screens.

For example, depending on the screen width, you could have the grid view display one or two columns to show more information and leverage the real estate of a bigger screen, as shown below:

Building the Grid Item

Still within restaurant_page.dart locate // TODO: Build Grid Item and replace it with the following:

Widget _buildGridItem(int index) {
  final item = widget.restaurant.items[index];
  return InkWell(
    onTap: () {
      // Present Bottom Sheet in the future.
    },
    child: RestaurantItem(item: item),
  );
}

This function takes an index and uses it to access a specific item from the restaurant’s menu. It then creates a RestaurantItem widget for that menu item. By wrapping the widget in an InkWell, we lay the groundwork for interactive functionality, such as opening a detail view in a bottom sheet upon tapping, which will be implemented in the next chapter.

Add this import at the top of the file:

import '../components/restaurant_item.dart';

Building the Section Title

Next, locate // TODO: Build Section Title and replace it with the following:

Widget _sectionTitle(String title) {
  return Padding(
    padding: const EdgeInsets.all(8.0),
    child: Text(
      title,
      style: const TextStyle(
        fontSize: 24,
        fontWeight: FontWeight.bold,),
    ),
  );
}

Here you simply create a Text with some custom padding.

Building the Grid View

Replace // TODO: Build Grid View with the following:

// 1
GridView _buildGridView(int columns) {
  // 2
  return GridView.builder(
    padding: const EdgeInsets.all(0),
    // 3
    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
      mainAxisSpacing: 16,
      crossAxisSpacing: 16,
      childAspectRatio: 3.5,
      crossAxisCount: columns,
    ),
    // 4
    itemBuilder: (context, index) => _buildGridItem(index),
    // 5
    itemCount: widget.restaurant.items.length,
    // 6
    shrinkWrap: true,
    // 7
    physics: const NeverScrollableScrollPhysics(),
  );
}

Here’s how the code works:

  1. The function _buildGridView() accepts the number of columns as a parameter. This is used to determine the number of columns to display on different devices.
  2. GridView.builder() is used for efficient, on-demand building of grid items.
  3. Set up SliverGridDelegateWithFixedCrossAxisCount to define the grid’s column count and spacing.
  4. Each grid item is built via _buildGridItem(), called within the itemBuilder callback.
  5. Set the number of items to display in the grid.
  6. The shrinkWrap property is enabled, allowing the GridView to size itself according to its children vertically.
  7. Set the physics to NeverScrollableScrollPhysics, to prevent scrolling within the grid itself.

Now that you’ve created your grid view, it is time to wrap it in a sliver.

Building the Grid View Sliver Section

Find // TODO: Add Desktop Threshold and replace it with the following:

static const desktopThreshold = 700;

This constant is used to determine whether to adapt the restaurant menu layout to big or small screens.

You need to calculate the number of columns depending on the screen’s width. Replace // TODO: Calculate Column Count with:

int calculateColumnCount(double screenWidth) {
  return screenWidth > desktopThreshold ? 2 : 1;
}

Depending on the screen width the function will either return 2 or 1.

Find // TODO: Build Grid View Section and replace it with the following code:

// 1
SliverToBoxAdapter _buildGridViewSection(String title) {
  // 2
  final columns = calculateColumnCount(MediaQuery.of(context).size.width);
  // 3
  return SliverToBoxAdapter(
    // 4
    child: Container(
      padding: const EdgeInsets.all(16.0),
      // 5
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 6
          _sectionTitle(title),
          // 7
          _buildGridView(columns),
        ],
      ),
    ),
  );
}

Here’s how the code works:

  1. Create a section with a title and grid view.
  2. Calculate the number of columns based on the screen’s width.
  3. Build a SliverToBoxAdapter to embed a non-sliver widget.
  4. Initialize a container for content with some padding.
  5. Set up a vertical layout with a Column widget.
  6. Add a section title using a custom method.
  7. Add a grid view with the specified number of columns.

Finally, locate // TODO: Add Menu Item Grid View Section and replace it and the sliver placeholder widget with the following:

_buildGridViewSection('Menu'),

Perform a hot reload, tap on a restaurant, and you should see the following on mobile or web:

When building for multiple platforms, it’s important to make sure that your app looks great on each platform. A web app has more screen real estate than a mobile app, so it’s important to make sure that the menu is responsive and adapts to the screen size. That’s what you’ll do in the next section.

Implementing a Responsive Menu

Crafting a responsive restaurant menu for web applications is a pivotal task that strikes a balance between optimal use of screen real estate, readability, and visual appeal.

When transitioning from mobile to web views, it’s crucial to consider how the menu’s layout adapts to larger screens. Let’s explore two primary strategies:

  • Option 1: Full-Screen Stretch - This approach extends the menu across the full width of the screen. While it maximizes space, it can also make the menu appear too large. The vastness can overwhelm users, presenting too much information at once and detracting from the ability to focus on individual items.

  • Option 2: Fixed Width - The alternative is to constrain the menu within a fixed-width container. This design is more aligned with standard web browsing expectations. It offers a structured layout with ample white space around the menu, which enhances visual appeal and improves content legibility.

Implementing Responsive Design

Next, you’ll delve into the technical implementation to achieve a responsive and visually pleasant menu for web users.

Still in restaurant_page.dart, locate the comment // TODO: Add Constraint Properties and replace it with the following:

static const double largeScreenPercentage = 0.9;
static const double maxWidth = 1000;

These constants represent the maximum allowable width and the percentage of screen width the menu should occupy on larger screens.

Next, locate the comment // TODO: Calculate Constrained Width and replace it with the following:

double _calculateConstrainedWidth(double screenWidth) {
  return (screenWidth > desktopThreshold
          ? screenWidth * largeScreenPercentage //
          : screenWidth)
      .clamp(0.0, maxWidth);
}

This function ensures that on larger screens the menu width is proportional to the screen size, up to a maximum width.

Now find // TODO: Replace build method and replace it and the entire build() method with the following:

@override
Widget build(BuildContext context) {
  final screenWidth = MediaQuery.of(context).size.width;
  final constrainedWidth = _calculateConstrainedWidth(screenWidth);

  return Scaffold(
    body: Center(
      child: SizedBox(
        width: constrainedWidth,
        child: _buildCustomScrollView(),
      ),
    ),
  );
}

With these changes, your restaurant menu will dynamically adapt to both mobile and web screen sizes. The result is a seamless user experience across devices.

Perform a hot reload, build, run your app and look at the restaurant menu on both mobile and web.

Try resizing your web browser window. You’ll observe how elegantly your restaurant menu adapts to various screen sizes – a testament to responsive design in action!

And there you have it – a responsive, visually appealing, and user-friendly restaurant menu for your Flutter application!

Key Points

  • Slivers allow building intricate scrolling layouts with CustomScrollView and various sliver widgets.
  • With GridView you can create grid layouts with customizable columns and spacing.
  • SliverToBoxAdapter enables the integration of non-sliver widgets into sliver lists.
  • Manage scroll behavior and direction with properties like physics.
  • Embed grid views within sliver lists for complex scrollable structures.
  • Use MediaQuery to create responsive grid layouts with GridView, adjusting the number of columns based on the screen size.

Where to Go From Here?

Now that you’ve a grasp of slivers and grid views in Flutter, you can create complex and custom scrollable layouts that look good and perform well on a wide range of devices. Slivers enable you to make scrollable areas in your app that look and behave exactly how you want.

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.