image

04 Jan 2026

9K

35K

Building an Infinite Scroll List Widget in Flutter

Introduction

Infinite scroll, also known as endless scrolling, is a popular user interface pattern where content loads continuously as the user scrolls down a page, eliminating the need for pagination. This technique greatly enhances user experience by providing a seamless browsing experience, commonly seen in social media feeds and large data catalogs. In Flutter, implementing an infinite scroll list is straightforward yet powerful, leveraging efficient widgets and robust state management. This article will guide you through creating a performant infinite scroll list widget in Flutter.

Core Concepts

To build an infinite scroll list in Flutter, we primarily rely on three core concepts:

1. ListView.builder

Unlike a regular ListView, which creates all its children at once, ListView.builder creates items lazily (only when they are visible on screen). This makes it highly efficient for displaying a large or infinite number of items, as it conserves memory and processing power.

2. ScrollController

A ScrollController allows you to control and observe the scroll position of a scrollable widget like ListView. We will use it to listen for scroll events and detect when the user has scrolled to the end of the list, signaling that more data needs to be fetched.

3. State Management for Data Loading

To manage the state of our list (items, loading status, and whether more data is available), we'll use a StatefulWidget. Key state variables will include a list of items, a boolean to indicate if data is currently being loaded (_isLoading), and another boolean to know if there's more data to fetch (_hasMore).

Step-by-Step Implementation

1. Initial Project Setup

First, create a new Flutter project:


flutter create infinite_scroll_list
cd infinite_scroll_list

Open lib/main.dart and clear its content, then proceed with the following steps.

2. The Data Model

Let's define a simple data model for the items in our list. For this example, we'll use a basic Item class:


class Item {
  final int id;
  final String title;

  Item({required this.id, required this.title});
}

3. The Main Widget (InfiniteScrollListScreen)

We'll create a StatefulWidget called InfiniteScrollListScreen to manage our list's state and behavior.

State Variables

Inside the _InfiniteScrollListScreenState class, declare the necessary state variables:


import 'package:flutter/material.dart';

class InfiniteScrollListScreen extends StatefulWidget {
  @override
  _InfiniteScrollListScreenState createState() => _InfiniteScrollListScreenState();
}

class _InfiniteScrollListScreenState extends State {
  final ScrollController _scrollController = ScrollController();
  List _items = [];
  bool _isLoading = false;
  bool _hasMore = true; // Initially assume there's more data to load
  int _page = 0;
  final int _limit = 20; // Number of items to load per page

  // ... rest of the code
}
_loadMoreItems Function

This function will simulate fetching new data. In a real application, this would involve making an API call. For demonstration, we'll use a Future.delayed to simulate network latency and generate dummy items.


  Future _loadMoreItems() async {
    if (_isLoading || !_hasMore) return;

    setState(() {
      _isLoading = true;
    });

    // Simulate network delay
    await Future.delayed(const Duration(seconds: 2));

    // Simulate fetching data
    final List newItems = List.generate(
      _limit,
      (index) => Item(id: _page * _limit + index, title: 'Item ${_page * _limit + index + 1}'),
    );

    if (newItems.isEmpty) {
      // No more items to load
      setState(() {
        _hasMore = false;
        _isLoading = false;
      });
      return;
    }

    setState(() {
      _items.addAll(newItems);
      _page++;
      _isLoading = false;
      // You might set _hasMore to false here if your API indicates no more data
      // For this example, we'll let it continue until a condition is met
      if (_items.length >= 100) { // Example: stop after 100 items
        _hasMore = false;
      }
    });
  }
_scrollListener Function

This listener will be attached to our _scrollController. It checks if the user has scrolled to the very end of the list and if new data is not currently being loaded. If both conditions are met, it triggers the _loadMoreItems function.


  void _scrollListener() {
    if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
      _loadMoreItems();
    }
  }
initState and dispose

Initialize the _scrollController and add the listener in initState. Also, make sure to dispose of the controller to prevent memory leaks in the dispose method.


  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_scrollListener);
    _loadMoreItems(); // Load initial data
  }

  @override
  void dispose() {
    _scrollController.removeListener(_scrollListener);
    _scrollController.dispose();
    super.dispose();
  }
build Method

The build method constructs the UI. We'll use a Scaffold with an AppBar and a ListView.builder. The itemCount will include an extra slot for the loading indicator at the bottom if _isLoading is true and _hasMore is true. The itemBuilder will conditionally render either an item or a loading spinner.


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Infinite Scroll List'),
      ),
      body: ListView.builder(
        controller: _scrollController,
        itemCount: _items.length + (_isLoading && _hasMore ? 1 : 0), // Add 1 for loading indicator
        itemBuilder: (context, index) {
          if (index == _items.length) {
            // This is the last item, show a loading indicator
            return const Padding(
              padding: EdgeInsets.all(8.0),
              child: Center(
                child: CircularProgressIndicator(),
              ),
            );
          }
          // Display the actual item
          final item = _items[index];
          return Card(
            margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
            child: ListTile(
              leading: CircleAvatar(child: Text('${item.id + 1}')),
              title: Text(item.title),
              subtitle: Text('ID: ${item.id}'),
            ),
          );
        },
      ),
    );
  }

4. Putting it all together (main.dart)

Your complete lib/main.dart file should look like this:


import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Infinite Scroll List',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: InfiniteScrollListScreen(),
    );
  }
}

// Data Model
class Item {
  final int id;
  final String title;

  Item({required this.id, required this.title});
}

// Infinite Scroll List Screen
class InfiniteScrollListScreen extends StatefulWidget {
  @override
  _InfiniteScrollListScreenState createState() => _InfiniteScrollListScreenState();
}

class _InfiniteScrollListScreenState extends State {
  final ScrollController _scrollController = ScrollController();
  List _items = [];
  bool _isLoading = false;
  bool _hasMore = true; // Initially assume there's more data to load
  int _page = 0;
  final int _limit = 20; // Number of items to load per page

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(_scrollListener);
    _loadMoreItems(); // Load initial data
  }

  @override
  void dispose() {
    _scrollController.removeListener(_scrollListener);
    _scrollController.dispose();
    super.dispose();
  }

  Future _loadMoreItems() async {
    if (_isLoading || !_hasMore) return;

    setState(() {
      _isLoading = true;
    });

    // Simulate network delay
    await Future.delayed(const Duration(seconds: 2));

    // Simulate fetching data
    // In a real app, you would fetch data from an API here
    final List newItems = List.generate(
      _limit,
      (index) => Item(id: _page * _limit + index, title: 'Item ${_page * _limit + index + 1}'),
    );

    if (newItems.isEmpty) {
      // No more items to load
      setState(() {
        _hasMore = false;
        _isLoading = false;
      });
      return;
    }

    setState(() {
      _items.addAll(newItems);
      _page++;
      _isLoading = false;
      // Example: Stop loading after 100 items for demonstration
      if (_items.length >= 100) {
        _hasMore = false;
      }
    });
  }

  void _scrollListener() {
    if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
      _loadMoreItems();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Infinite Scroll List'),
      ),
      body: ListView.builder(
        controller: _scrollController,
        itemCount: _items.length + (_isLoading && _hasMore ? 1 : 0), // Add 1 for loading indicator
        itemBuilder: (context, index) {
          if (index == _items.length) {
            // This is the last item, show a loading indicator
            return const Padding(
              padding: EdgeInsets.all(8.0),
              child: Center(
                child: CircularProgressIndicator(),
              ),
            );
          }
          // Display the actual item
          final item = _items[index];
          return Card(
            margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
            child: ListTile(
              leading: CircleAvatar(child: Text('${item.id + 1}')),
              title: Text(item.title),
              subtitle: Text('ID: ${item.id}'),
            ),
          );
        },
      ),
    );
  }
}

Conclusion

You have successfully implemented an infinite scroll list widget in Flutter. By combining ListView.builder for efficient rendering, ScrollController for scroll detection, and proper state management, you can create a smooth and responsive user experience for displaying large datasets. This pattern is highly adaptable and can be integrated with various backend APIs and state management solutions (like Provider, Riverpod, BLoC) for more complex applications.

Further enhancements could include adding pull-to-refresh functionality, error handling for network requests, and more sophisticated loading indicators or "no more data" messages.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is