Skip to content
FlutterLearn

Beginner · Lesson 7 · 12 min read

Lists, Grids and Scrolling

Render long lists efficiently with ListView.builder, lay out grids, add pull-to-refresh and swipe-to-delete, and meet slivers.

Updated August 1, 2026

What you will learn

  • Choose the right ListView constructor for the job
  • Build grids that adapt to screen width
  • Add pull-to-refresh, swipe-to-delete and reordering
  • Understand what slivers are and when you need them

Most app screens are a list of something. Flutter's scrolling widgets are excellent, but choosing the wrong one is the single most common cause of a beginner app feeling slow — so this lesson is as much about which widget as it is about how to use it.

Four ways to build a list

ConstructorBuildsUse when
ListView(children: [...])Everything, immediatelyA short, fixed list — a settings screen
ListView.builderOnly visible items, lazilyLong or unknown-length lists — the default choice
ListView.separatedLazily, with dividers betweenLists that need separators
ListView.customWhatever your delegate saysRare — custom child management
Dart
// Short and fixed: fine to build everything up front
ListView(
  padding: const EdgeInsets.all(16),
  children: const [
    ListTile(leading: Icon(Icons.person), title: Text('Account')),
    ListTile(leading: Icon(Icons.lock), title: Text('Privacy')),
    ListTile(leading: Icon(Icons.info), title: Text('About')),
  ],
)

// Long: only builds what is on screen (plus a small buffer)
ListView.builder(
  itemCount: articles.length,
  itemBuilder: (context, index) {
    final article = articles[index];
    return ListTile(
      title: Text(article.title),
      subtitle: Text('${article.minutes} min read'),
      onTap: () => _open(article),
    );
  },
)

// With dividers — note separatorBuilder gets its own index
ListView.separated(
  itemCount: articles.length,
  separatorBuilder: (context, index) => const Divider(height: 1),
  itemBuilder: (context, index) => ArticleTile(article: articles[index]),
)

Grids

Dart
// Fixed number of columns — simple, but wrong on tablets
GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    mainAxisSpacing: 12,
    crossAxisSpacing: 12,
    childAspectRatio: 3 / 4,
  ),
  itemCount: products.length,
  itemBuilder: (context, i) => ProductCard(product: products[i]),
)

// Better: fix the maximum tile width and let the column count follow
GridView.builder(
  padding: const EdgeInsets.all(16),
  gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
    maxCrossAxisExtent: 220,
    mainAxisSpacing: 12,
    crossAxisSpacing: 12,
  ),
  itemCount: products.length,
  itemBuilder: (context, i) => ProductCard(product: products[i]),
)

childAspectRatio is width divided by height. If your grid tiles overflow vertically, that ratio is usually the culprit — not the tile's contents.

Pull to refresh

Dart
RefreshIndicator(
  onRefresh: () async {
    final fresh = await api.fetchArticles();
    if (!mounted) return;
    setState(() => _articles = fresh);
  },
  child: ListView.builder(
    // Keeps the gesture working even when the list is too short to scroll
    physics: const AlwaysScrollableScrollPhysics(),
    itemCount: _articles.length,
    itemBuilder: (context, i) => ArticleTile(article: _articles[i]),
  ),
)

Swipe to delete

Dart
Dismissible(
  // The key must be stable and unique, or the wrong row disappears
  key: ValueKey(task.id),
  direction: DismissDirection.endToStart,
  background: Container(
    alignment: Alignment.centerRight,
    padding: const EdgeInsets.only(right: 20),
    color: Theme.of(context).colorScheme.errorContainer,
    child: const Icon(Icons.delete_outline),
  ),
  confirmDismiss: (direction) async {
    return await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Delete task?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: const Text('Cancel'),
          ),
          FilledButton(
            onPressed: () => Navigator.pop(context, true),
            child: const Text('Delete'),
          ),
        ],
      ),
    );
  },
  onDismissed: (direction) {
    setState(() => _tasks.remove(task));
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: const Text('Task deleted'),
        action: SnackBarAction(
          label: 'Undo',
          onPressed: () => setState(() => _tasks.insert(index, task)),
        ),
      ),
    );
  },
  child: TaskTile(task: task),
)

Reorderable lists

Dart
ReorderableListView.builder(
  itemCount: _tasks.length,
  onReorder: (oldIndex, newIndex) {
    setState(() {
      // The framework reports newIndex before the removal
      if (newIndex > oldIndex) newIndex -= 1;
      final task = _tasks.removeAt(oldIndex);
      _tasks.insert(newIndex, task);
    });
  },
  itemBuilder: (context, index) {
    final task = _tasks[index];
    return ListTile(
      key: ValueKey(task.id),
      title: Text(task.title),
      trailing: const Icon(Icons.drag_handle),
    );
  },
)

Slivers: scrolling beyond a plain list

A sliver is a piece of a scrollable area that knows how to behave as it scrolls in and out of view. You need them when one scroll view has to contain several different sections — a collapsing header, then a grid, then a list.

Dart
CustomScrollView(
  slivers: [
    // Collapses into a compact bar as you scroll
    SliverAppBar.large(
      title: const Text('Discover'),
      floating: true,
      flexibleSpace: FlexibleSpaceBar(
        background: Image.network(headerUrl, fit: BoxFit.cover),
      ),
    ),

    // A normal widget inside a sliver world
    const SliverToBoxAdapter(
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Text('Popular this week'),
      ),
    ),

    SliverGrid.builder(
      gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
        maxCrossAxisExtent: 200,
      ),
      itemCount: featured.length,
      itemBuilder: (context, i) => ProductCard(product: featured[i]),
    ),

    SliverList.builder(
      itemCount: articles.length,
      itemBuilder: (context, i) => ArticleTile(article: articles[i]),
    ),
  ],
)

Scroll controllers and infinite scroll

Dart
final _controller = ScrollController();
bool _loadingMore = false;

@override
void initState() {
  super.initState();
  _controller.addListener(_onScroll);
}

@override
void dispose() {
  _controller
    ..removeListener(_onScroll)
    ..dispose();
  super.dispose();
}

void _onScroll() {
  final position = _controller.position;
  // Start loading before the user actually hits the bottom
  if (position.pixels >= position.maxScrollExtent - 400 && !_loadingMore) {
    _loadNextPage();
  }
}

Loading the next page a few hundred pixels early is what makes infinite scroll feel seamless rather than stuttering at every page boundary.

Official references

Continue with the primary Flutter and Dart documentation for this topic.

Key takeaways

  • ListView.builder is the default for any list whose length comes from data.
  • Grids should size by maximum tile extent, not a fixed column count.
  • Anything that can be removed or reordered needs a stable key from the item's id.
  • Use CustomScrollView with slivers when one scroll view holds several kinds of section.

Practice

A feed with everything

Build a screen with a collapsing SliverAppBar, a horizontal 'featured' row, and a long paginated list below it. Add pull-to-refresh, swipe-to-delete with an undo snackbar, and load the next page 400 pixels before the bottom.

Show hints
  • A horizontal list inside a vertical scroll needs a fixed height — wrap it in a SizedBox.
  • Keep the page number and loading flag in State, and guard against firing two loads at once.

Lesson 11

Build a To-Do App from Scratch

Turn widgets, lists, text input and local state into a complete first Flutter app with add, complete, filter and delete flows.

Lesson 6

Navigation Between Screens

Push and pop screens, pass data both ways, and set up named routes — plus when you should graduate to a router package.

Lesson 8

Images, Assets, Icons and Fonts

Bundle images and fonts properly, load network images with loading and error states, and understand resolution-aware assets.