skip to content

In a Flutter CustomScrollView, what does SliverFillRemaining do, and what do its hasScrollBody and fillOverscroll flags change?

level: middleimportance: nice to knowfreq 20%

answer

  1. usually the last sliver
  2. fills what the viewport has left
  3. hasScrollBody defaults to true
  4. false: sized to leftover or child
  5. fillOverscroll only when hasScrollBody false

basics

~20 s

SliverFillRemaining gives one box child the viewport space left after the preceding slivers. hasScrollBody (default true) treats the child as scrollable and reserves a full viewport; false sizes it to the leftover space or the child, whichever is larger. fillOverscroll stretches it into bounce overscroll.

solid answer

~40 s

`SliverFillRemaining` is a single-box sliver, normally last in a `CustomScrollView`, that sizes its child to the space the viewport has left. With the default `hasScrollBody: true` it assumes the child scrolls on its own: the child is forced to the remaining paint extent and the sliver reports a full viewport of scroll extent, so a page with short content can still scroll. With `hasScrollBody: false` it takes the viewport height minus what the earlier slivers already used, or the child's intrinsic height if that is larger, so an empty state or a bottom-aligned 'Book now' button sits at the bottom with no extra scroll. `fillOverscroll` (default `false`) matters only with `hasScrollBody: false`: on bouncing physics it stretches the child into the overscroll area.

code

dart · 31 lines
dart
import 'package:flutter/material.dart';

class RoomPicker extends StatelessWidget {
  const RoomPicker({super.key, required this.rooms, required this.onBook});

  final List<String> rooms;
  final VoidCallback onBook;

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        const SliverAppBar(pinned: true, title: Text('Choose a room')),
        SliverList.builder(
          itemCount: rooms.length,
          itemBuilder: (context, index) => ListTile(title: Text(rooms[index])),
        ),
        SliverFillRemaining(
          hasScrollBody: false,
          child: Align(
            alignment: Alignment.bottomCenter,
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: FilledButton(onPressed: onBook, child: const Text('Book now')),
            ),
          ),
        ),
      ],
    );
  }
}

go deeper

for a junior

Remember that SliverFillRemaining fills the space left at the end of a CustomScrollView and holds one child.

for a middle

Explain the two hasScrollBody paths, the default of true, and why fillOverscroll matters only on bouncing physics.

for a senior

Recognise the extra-scroll bug and the infinite-extent assertion, and keep footer subtrees cheap to measure.

for a principal

Decide whether a bottom action belongs in the scroll or in a fixed bar outside it, for consistency across screens.

## What it does `SliverFillRemaining` is a sliver that holds **one box child** and sizes it to the space the viewport has **left** after the slivers before it. It fills the cross axis completely; the main-axis size depends on two flags. By definition nothing fits after it, so it is typically the **last** sliver. Typical uses on a hotel-detail page: - a **'Book now' bar** that sits at the bottom of the screen when the room list is short, and after the list when it is long; - an **empty state** ('No reviews yet') centred in whatever space the header leaves; - a **login or confirmation footer** below a collapsing header. ## hasScrollBody: the flag that surprises people `hasScrollBody` defaults to **`true`**, which is rarely what a static footer wants. | | `hasScrollBody: true` (default) | `hasScrollBody: false` | |---|---|---| | Assumes the child | scrolls on its own (a nested list) | is static content | | Child main-axis size | forced to the remaining paint extent | viewport extent minus `precedingScrollExtent`, or the child's max intrinsic height if larger | | Sliver's scroll extent | one full viewport, whatever came before | exactly the size above | | Effect on a short page | the page can scroll by extra space | no extra scroll; content fits exactly | | Intrinsic measurement | none | measures the child's max intrinsic height | With the default, the render object does not look at the child's own size at all: it hands the child exactly the visible remaining space and reports a full viewport of scroll extent, because an inner scrollable could be of any length. That is right for a nested scrollable body and wrong for a button bar, where it shows up as a page that scrolls when it should not. With `false`, the sliver computes the viewport's main-axis extent minus the scroll extent of everything before it. If the child is taller than that, the child wins and the sliver simply becomes as tall as the child; if the earlier slivers already overflow the screen, the leftover is zero or negative and the child keeps its own height. ## fillOverscroll `fillOverscroll` defaults to `false` and is **only relevant when `hasScrollBody` is `false`**. On scroll physics that allow overscroll — the iOS-style bouncing physics — setting it to `true` lets the child stretch into the overscroll gap at the end of the list, so a coloured footer does not reveal a blank strip during the bounce. On clamping physics there is no overscroll area, so it changes nothing visible. ## Pitfalls 1. **Leaving the default on a static footer.** The page scrolls by nearly a screen of empty space; set `hasScrollBody: false`. 2. **A scrollable child with `hasScrollBody: false`.** The sliver asks the child for its intrinsic height; a list reports an unbounded one and the framework asserts that the calculated extent is not finite, suggesting `hasScrollBody` should not be `false`. 3. **Expensive children with `false`.** Intrinsic measurement lays the subtree out speculatively; keep the footer subtree small. 4. **Placing it mid-list.** Anything after it starts beyond the filled space, which is rarely the intent. ## Related fill slivers - **`SliverFillViewport`** — a lazy list whose every child is exactly one viewport tall, times `viewportFraction` (default `1.0`); a vertical pager built from slivers. - **`SliverFillRemaining`** — one child sized to the leftover, as above. Most developers never touch these, which is why interviewers use the `hasScrollBody` default as a differentiator: a candidate who has shipped a bottom-aligned button inside a `CustomScrollView` has usually met the extra-scroll bug and its one-line fix.

  • Why does a short page with a default SliverFillRemaining footer still scroll?
    With `hasScrollBody: true` the sliver reports a full viewport of scroll extent regardless of what precedes it, because it assumes its child is itself scrollable. The slivers above plus one viewport exceed the screen, so the page scrolls. Setting `hasScrollBody: false` sizes the sliver to the actual leftover space instead.
  • What goes wrong if the child is a ListView and hasScrollBody is false?
    With `false` the sliver measures the child's max intrinsic height to decide its extent. A scrollable cannot report a finite one, so the framework asserts that the calculated extent is not finite and points you to `hasScrollBody`, which should stay `true` for a scrollable child.

saying these in an interview costs you the question

  • hasScrollBody defaults to false.
  • SliverFillRemaining can be placed anywhere without affecting later slivers.
  • fillOverscroll has an effect when hasScrollBody is true.
  • With hasScrollBody false the child is clipped to the leftover space.
  • SliverFillRemaining lazily builds a list of children.