Spring Ahead Sale — Save on Everything.All videos. All books. Now 50% off.

Build your mobile development skills and save! Stay ahead of the rest with an Ultimate book & video subscription. Starting at just $149/year as part of the Spring Ahead sale.

Home Flutter Tutorials

Slivers in Flutter: Getting Started

In this article you’ll learn about Slivers in Flutter, how they work, and use them to make a beautifully designed app for recipes.

4.6/5 5 Ratings

Version

  • Dart 2.6, Flutter 1.22, Android Studio 4.1

Have you ever wondered how lists and grids work in Flutter? Have you ever wanted to make a complex effect related to scrolling? Do the words complex and impossible echo in your mind once someone mentions slivers?

Well, today is your lucky day! You’re about to start a trip through the enigmatic sliver universe.

In Flutter, a sliver is a slice of a scrollable area you can use to achieve custom scrolling behaviors. When you finish this tutorial, you’ll know more about slivers and be comfortable using them.

Along the way, you’ll learn:

  • What slivers are and how they work,
  • When to use a sliver.
  • How to work with different types of slivers.
Note: This tutorial assumes you have experience with Flutter and Flutter widgets. If you don’t, check out this Flutter UI Widgets video course and Getting Started with Flutter tutorial.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the page.

In 2018, a tweet showcasing food recipes written by a pre-k class went viral. In this tutorial, you’ll work on Reciperlich, a fun app that displays these recipes.

The app has two pages. The first page displays a list of recipes. When you click a recipe, you route to the other page to view the recipe details.

Recipe Page Final
Recipe Page Final

This tutorial will use Android Studio 4.1, so some of the screenshots might be specific to it, but feel free to use Visual Studio Code if you’re more comfortable with it.

Choose Open an existing Android Studio Project. Then choose the starter folder from the tutorial materials.

Download the dependencies by opening pubspec.yaml from Android Studio and clicking Pub get at the top.

Install third-party dependencies

Finally, build and run. You’ll see a placeholder page.

starter

Now that you’re up and running, time to take a deeper look into Slivers before writing some actual code.

Understanding Slivers

You probably know your way around StatefulWidgets and StatelessWidgets. Did you know they’re not Flutter’s only widgets? Flutter also uses RenderObjectWidgets.

RenderObjecsWidgets act as blueprints that hold the configuration information for RenderObjects. As suggested by their name, these objects are responsible for rendering. They form the basic infrastructure for managing the visual elements tree and defining the layout, painting and composting protocols.

Widget Subclasses and RenderObject Diagram

Flutter creates layouts by organizing widgets in trees. When Flutter creates a new widget, the parent widget passes constraint information to its children. RenderObjects work to paint everything.

A RenderBox comprises several RenderObject, like Container or SizedBoxes, and follow box protocol. This protocol lets each widget know its constraints when it’s rendered.

RenderBox only has cartesian corners: width and height. RenderObject paints the layout when a parent object gives its child object the minimum and maximum of both the width and height.

While this works great with boxes, it doesn’t work for scrolling or things like app bar animation and special effects. Consequently, Flutter built a different type of RenderObject for scrolling — RenderSliver. It uses a different protocol, the sliver protocol, to let the RenderObject receive additional constraints from its parent with more axes to ensure the RenderObject only renders when it’s in the viewport.

Slivers Constraints

You might ask yourself – why would I want to use a different kind of RenderObject for scrolling? RenderSliver lets you render child widgets lazily or render only the visible portion of the screen. This makes scrolling large lists efficient.

For example, both ListView and GridView use RenderSliver underneath the hood. Anything that scrolls in Flutter uses slivers except OneBoxScrollView as it doesn’t require lazy loading.

Displaying Recipe List

The starter project has many files. However, the most important are:

  1. lib/data/recipe_repository.dart: A class that returns a static constant list of RecipeModels.
  2. lib/pages/recipe_list/widgets/recipe_item_widget.dart: A widget that accepts a RecipeModel and displays a recipe list tile.

The starter code has an empty recipe list page, which opens once you open the app. You’ll display the recipes list in a SliverList.

Replace the content of lib/pages/recipe_list/recipe_list_page.dart with:

import 'package:flutter/material.dart';
import 'package:reciperlich/data/recipe_repository.dart';

import 'widgets/recipe_item_widget.dart';

class RecipeListPage extends StatelessWidget {
  const RecipeListPage({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // 1
    final recipes = RecipeRepository.getRecipes();

    return Scaffold(
      // 2
      body: CustomScrollView(
      // 3
      slivers: <Widget>[
           // TODO: Add a cool AppBar

           // 4
           SliverFixedExtentList(
              // 5
              itemExtent: 110,
              // 6
              delegate: SliverChildBuilderDelegate(
                    (context, index) => Padding(
                    padding: const EdgeInsets.only(bottom: 20),
                    // 7
                    child: RecipeItem(recipes[index])),
                childCount: recipes.length,
              ),
            ),
        ],
      ),
    );
  }
}

This code is a bit long, but no worries, let’s break it down.

In the code above, you:

  1. Fetch the recipe list from the repository.
  2. Define the main widget where you’ll put all your slivers.
  3. Use a CustomScrollView that has a slivers argument, instead of the commonly used children, to remind you that all its children must produce RenderSlivers.
  4. SliverFixedExtentList returns a sliver that displays a linear list of children, all with the same extent, width or height, depending on the main axis.
  5. Determine the item extent of a single recipe item. In this case, the extent is the height.
  6. Determine the delegate type as SliverChildBuilderDelegate and give this delegate the childCount which is the recipes’ count. The delegate returns an index value.
  7. The child itself is a RecipeItem which receives a RecipeModel from the list of recipes.
Note: Up to this point, you could only use ListView. It’s a wrapper that wraps around SliverList or SliverFixedExtentList if it has itemExtent. However, you’ll want to eventually add more slivers to the CustomScrollView.

You could’ve used SliverChildListDelegate instead of SliverChildBuilderDelegate. SliverChildListDelegate is a delegate that supplies the list of RecipeItem widgets explicitly without a builder that provides an index. It looks like this:

delegate: SliverChildListDelegate(
  recipes
      .map((e) => Padding(
      padding: const EdgeInsets.only(bottom: 20),
      child: RecipeItem(e)))
      .toList(),
),

However, SliverChildListDelegate requires building all the widgets in advance which isn’t efficient and reduces the benefit of building children lazily. So SliverChildBuilderDelegate is the better choice.

Build and run. Tada! Now you have a list of recipes.

Recipe List Page _1

You’ve just used SliverFixedExtentList but you didn’t really stop to understand why, and why not use the before-mentioned SliverList instead. Time to dive deeper into these topics.

SliverFixedExtentList and SliverList

As you may have guessed from their names, SliverList is more generic and flexible compared to SliverFixedExtentList. The latter assumes a fixed extent for its children, while the former doesn’t specify an extent and renders the children widgets with their constraints.

If you wanted to rewrite the recipe list using SliverList instead of SliverFixedExtentList you could do so by wrapping RecipeItem with a height constraint. No need to make this change in your code, but it’s here for your reference:

SliverList(
  delegate: SliverChildBuilderDelegate(
    (context, index) => SizedBox(
      height: 110,
      child: Padding(
          padding: const EdgeInsets.only(bottom: 20),
          child: RecipeItem(recipes[index])),
    ),
    childCount: recipes.length,
  ),
),

Which of the two, SliverList and SliverFixedExtentList, do you think is more efficient?

[spoiler title=”SliverList vs SliverFixedExtentList”]
SliverFixedExtentList is highly more efficient than SliverList as it doesn’t need to calculate the extent of its children dynamically. Try to use SliverListExtentList as possible, and only resort to SliverList if you need to support children of varying or dynamic extents.
[/spoiler]

Time for you to extend Reciperlich with a nice scrollable App Bar.

Building a Scrollable App Bar

Now that you have a list of recipes, you need to add a cool scrollable app bar. Developers commonly use slivers to customize app bars with behavior different from Flutter’s default AppBar widget.

Since you’ll reuse the same app bar in the app’s two pages, create a new AppBarWidget in the shared folder.

In lib/shared_widgets, add a new file named app_bar_widget.dart and add the following code to it:

import 'package:flutter/material.dart';

import '../constants/colors.dart';
import 'image_with_top_shadow_widget.dart';

class AppBarWidget extends StatelessWidget {
  // 1
  final String text;
  final String imagePath;
  final bool centerTitle;

  const AppBarWidget({
    Key key,
    @required this.text,
    @required this.imagePath,
    this.centerTitle = false,
  }) : assert(text != null),
       assert(imagePath != null),
       super(key: key);

  @override
  Widget build(BuildContext context) {
    // 2
    return SliverAppBar(
      title: Text(
        text,
        style: const TextStyle(
          fontSize: 30,
          fontWeight: FontWeight.bold,
        ),
      ),
      backgroundColor: AppColors.navy,
      centerTitle: centerTitle,
      // 3
      expandedHeight: 200.0,
      // 4
      pinned: true,
      elevation: 0,
      // 5
      flexibleSpace: FlexibleSpaceBar(
        background: ImageWithTopShadowWidget(imagePath),
      ),
    );
  }
}

Here’s a step-by-step code breakdown:

  1. AppBarWidget takes two mandatory properties: the title text and image path with. It also takes a Boolean property which decides if it should center the text title, which default to false.
  2. SliverAppBar is a scrollable app bar. You give it the same properties as AppBar as well as properties that define the scrollability behavior.
  3. You provide the expandedHeight to set the app bar’s size when it’s fully expanded. By setting the value here, you make the app bar scrollable.
  4. When you set pinned to true, you let the app bar remain visible at the start of the scroll view.
  5. You add an image with a shadow widget stacked behind the toolbar and the tab bar. By adding it to FlexibleSpaceBar, you set its height to the same as the app bar’s overall height.

SliverAppBar has three Booleans in its configuration. When you change them, you alter the behavior of the app bar when scrolling:

  • pinned: When set to true, the app bar will remain visible as the user scrolls.
  • floating: By setting it to true, you make the app bar visible as soon as the user scrolls towards the app bar. Otherwise, they’ll need to scroll near the top of the scroll view to reveal the app bar.
  • snap: When true, the app bar will fully expand as you scroll, which is helpful when you have a text field in the app bar.

Check out this great interactive demo from Flutter documentation for these three Booleans.

Now that you added the shared AppBarWidget, you’ll add it to the recipe list page.

Reusing SliverAppBar

Back in lib/pages/recipe_list/recipe_list_page.dart, add an import to AppBarWidget at the top of the file:

import '../../constants/app_image_paths.dart';
import '../../shared_widgets/app_bar_widget.dart';

Now, replace // TODO: Add a cool AppBar with:

const AppBarWidget(
  text: 'Reciperlich',
  imagePath: AppImagePaths.mainImage,
  centerTitle: true,
),

Here’s what you just did:

  • Instead of adding AppBarWidget to the appBar in Scaffold, you inserted it directly as a child in CustomListView above the recipe list, since it is a sliver.
  • Then, you used AppImagePaths to provide the AppBarWidget with the main image path.

Build and run. You’ll see your recipe list page in action!

Recipe list without padding

Now, you’ll add AppBarWidget to the other page, RecipePage.

Go to lib/pages/recipe/recipe_page.dart and, at the top of the file, add:

import '../../shared_widgets/app_bar_widget.dart';

Next, replace // TODO: add an AppBarWidget with:

AppBarWidget(
  text: recipe.title,
  imagePath: recipe.mainImagePath,
),

Build and run to ensure the AppBarWidget works. Then, click any recipe item. When RecipePage opens, you’ll see the app bar behaves the same on both pages.

Recipe page with custom App Bar

Inserting a Footer to CustomScrollView

Now you have two slivers inside CustomScrollView in RecipeListPage. They animate together when you scroll.

But what if you want to add a widget that produces a RenderBox, like a SizedBox or a Container? And what if you want to add it to CustomScrollView and scroll it with the other slivers even though it’s not a sliver?

No worries! Flutter’s got your back. This is the perfect situation to use SliverToBoxAdapter to convert boxes to slivers.

You’ll now add a footer to the CustomScrollView in RecipeListPage. Go over to lib/pages/recipe_list/widgets/footer_widget.dart, where you’ll find FooterWidget. It returns a Column widget.

You’ll use it as a footer for RecipeListPage.

Go back to lib/pages/recipe_list/recipe_list_page.dart. Import FooterWidget at the top of the file:

import 'widgets/footer_widget.dart';

Then, add the following at the end of the slivers argument, right under SliverFixedExtentList inside CustomScrollView:

const SliverToBoxAdapter(
  child: FooterWidget(),
),

Build and run. See the footer when you scroll at the end. Isn’t it lovely?

Recipe List with Footer

Adjusting the Layout

The recipe list needs some padding. However, Flutter’s Padding widget doesn’t take slivers. You’ll need widgets like Padding, SafeArea, Opacity and Visibility but for slivers.

Once again, Flutter comes to the rescue! You’ll use classes that give the same functionality but take slivers: SliverPadding, SliverOpacity, SliverOpacity and SliverVisibility.

Note: The names of widgets that produce slivers with RenderSliver always start with Sliver. Hence, you have SliverAppBar, SliverGrid, SliverList and SliverPadding. This usually comes in handy when you use autocomplete in your IDE.

Wrap SliverFixedExtentList like this:

SliverPadding(
  padding: const EdgeInsets.all(20),
  sliver: SliverFixedExtentList(...),
),

Build and run. See that you’ve adjusted the padding.

Recipe Page Final

Controlling the SliverGrid Layout

You use SliverGrid as you would use SliverList, by giving it child boxes and getting a RenderSliver. However, you’ll get a two-dimensional arrangement of rendered children in SliverGrid rather than a linear arrangement. You control this arrangement using various constructors in SliverGrid:

  1. SliverGrid.Count: You display the children depending on a fixed number of cross-axis tiles that you pass to the constructor as crossAxisCount.
  2. SliverGrid.Extent: Similar to how you use SliverFixedExtentList for linear lists, you arrange the children so each one has a maximum cross-axis extent that you pass to the constructor. By specifying the maximum width of the items, you make SliverGrid determine how many of them should fit across the grid.
  3. SliverGrid: You use SliverGrid‘s default constructor and give it two delegates. The first, gridDelegate, determines the grid’s layout, either by cross-axis count or extent. The second, SliverChildBuilderDelegate, builds the children by determining their count and giving them an index to efficiently build the list of box children. SliverList uses SliverChildBuilderDelegate.

SliverGrid constructors

Adding the Ingredients Grid

Each recipe has a list of ingredients and a list of informational numbers, such as cooking time or number of servings. You’ll display each list in a separate SliverGrid.

Add the following import to the top of lib/pages/recipe/recipe_page.dart:

import 'widgets/pill_widget.dart';

Then, replace // TODO: SliverGrid for recipe.ingredients with:

SliverPadding(
  padding: const EdgeInsets.all(15),
  sliver: SliverGrid.count(
    //1
    mainAxisSpacing: 15, 
    crossAxisSpacing: 10,

    //2
    crossAxisCount: 3,
    
    //3
    childAspectRatio: 3,

    //4
    children: recipe.ingredients.map((e) => PillWidget(e)).toList(),
  ),
),

Here you:

  1. Define the main and cross spacing between the box children.
  2. Give the crossAxisCount a number that constrains the count of horizontal elements.
  3. By defining the ratio of the length to the width of the PillWidget, you constraint the height of each rendered element in the grid.
  4. Transform the list of ingredients to a list of PillWidgets.
Note: SliverGrid.count isn’t the most efficient way to render a SliverGrid, as it requires building the widget list in advance. Using the default SliverGrid constructor is considered more efficient.

Build and run. You’ll see the following screen:

Recipe Details SliverGrids

Adding the Numbers Grid

You’ll also add a SliverGrid for the numbers grid. However, you’ll use its default constructor.

In the same file, replace // TODO: SliverGrid for recipe.details with:

SliverPadding(
  padding: const EdgeInsets.all(15),
  sliver: SliverGrid(
    // 1
    gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
      maxCrossAxisExtent: 200.0,
      mainAxisSpacing: 10,
      crossAxisSpacing: 10,
      childAspectRatio: 4,
    ),
    // 2
    delegate: SliverChildBuilderDelegate(
          (context, index) => PillWidget(recipe.details[index]),
      childCount: recipe.details.length,
    ),
  ),
),

Here you:

  1. Define a gridDelegate by determining the maximum cross-axis extent per child.
  2. Set a builder delegate to efficiently build the widgets only when they’re in the viewport.

Build and run. You’ll see two grids like this:

Recipe details with two grids

Implementing SliverPersistentHeaderDelegate

While SliverAppBar is customizable, sometimes you need even more customization. Under the hood, SliverAppBar is a SliverPersistentHeader. This means you’ll find most of the properties that are in SliverAppBar in SliverPersistentHeader as well.

SliverPersistentHeader takes a delegate class that extends the abstract class SliverPersistentHeaderDelegate.

The recipe details page doesn’t have any subheader. You’ll use SliverPersistentHeader to make elegant, persistent subheaders that expand when you scroll down but persist with a minimum height when scrolling up. Then, you’ll insert these subheaders inside RecipePage‘s CustomListView.

Before creating the subheader, you’ll first create the delegate that SliverPersistentHeader accepts. Go to lib/pages/recipe/widgets/ and create a new file named sliver_sub_header.dart.

Add the following code to it:

import 'dart:math';
import 'package:flutter/material.dart';

 // 1
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  final double minHeight;
  final double maxHeight;
  final Widget child;

  _SliverAppBarDelegate({
    @required this.minHeight,
    @required this.maxHeight,
    @required this.child,
  });

  @override
  double get minExtent => minHeight;

  @override
  double get maxExtent => max(maxHeight, minHeight);

  // 2
  @override
  Widget build(
      BuildContext context, double shrinkOffset, bool overlapsContent) {
    return SizedBox.expand(child: child);
  }

  // 3
  @override
  bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
    return maxHeight != oldDelegate.maxHeight ||
        minHeight != oldDelegate.minHeight ||
        child != oldDelegate.child;
  }
}

Here’s what’s happening in the code above:

  1. _SliverAppBarDelegate is a private class that extends the abstract class SliverPersistentHeaderDelegate and overrides the implementation of minExtent and maxExtent.
  2. The implementation of the build function only builds the expanded child widget.
  3. You also override shouldRebuild so it rebuilds when any of these three properties change: maxHeight, minHeight or child.

Customizing a Subheader With SliverPersistentHeader

You built the delegate. Now, you need to create your custom SliverPersistentHeader.

While you’re still in lib/pages/recipe/widgets/sliver_sub_header.dart and above _SliverAppBarDelegate, add:

import '../../../constants/colors.dart';

class SliverSubHeader extends StatelessWidget {
  final String text;
  final Color backgroundColor;

  const SliverSubHeader(
      {Key key, @required this.text, @required this.backgroundColor})
      : assert(text != null),
        assert(backgroundColor != null),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    // 1
    return SliverPersistentHeader(
      pinned: true,
      delegate: _SliverAppBarDelegate(
        // 2
        minHeight: 40,
        maxHeight: 70,
        // 3
        child: Container(
          color: backgroundColor,
          child: Center(
            child: Text(
              text,
              style: const TextStyle(
                  color: AppColors.navy,
                  fontSize: 23,
                  fontWeight: FontWeight.bold),
            ),
          ),
        ),
      ),
    );
  }
}

Here’s a breakdown:

  1. SliverSubHeader returns a SliverSubHeader with a custom configuration. You’ll use it as a subheader in RecipePage.
  2. You pass the desired minimum and maximum height to the implemented delegate class.
  3. The delegate gets the subheader text as a child widget and displays it in the persistent header.

Are you excited to see how the subheader turned out?

Go to lib/pages/recipe/recipe_page.dart and add an import for your SliverSubHeader to the top, like this:

import 'widgets/sliver_sub_header.dart';

Replace all the // TODO: Subheader with text title: Xs with an equivalent instance of SliverSubHeader widget an use X as the value of text. For example,:

SliverSubHeader(
  text: 'Instruction',
  backgroundColor: recipe.itemColor,
),

Finally, for it to work as expected, you need to provide a SliverFillRemaining at the end of CustomScrollView. Replace // TODO: SliverFillRemaining with:

SliverFillRemaining(
  child: Container(),
),

This lets you scroll the CustomScrollView freely in the vertical axis as you fill the space with an empty container.

Mission accomplished! Build and run. You'll get something like this:

Recipe Page Final

Where to Go From Here?

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

Now you have a deeper understanding of what slivers are, and more importantly, when and how to use them. You used SliverLists and SliverGrids along with your SliverAppBar to have an interesting scrolling behavior.

Dive deeper in slivers by reading the Flutter documentation for RenderSliver.

If you have any questions, comments or want to show off great theming options for your app, feel free to join the discussion below!

Average Rating

4.6/5

Add a rating for this content

5 ratings

More like this

Contributors

Comments