Flutter: ListView Pagination (Load More) Example (updated)

Updated: August 5, 2023 By: A Goodman 7 comments

In real-world Flutter applications, we often load dynamic data from servers or databases instead of using hardcode/dummy data as we often see in online examples.

For instance, you have an e-commerce app and need to load products from Rest API from the server side. If you only have 10 or 20 products, it will be easy to fetch them all with just one GET request. However, if your platform has thousands of products or even millions of products (e.g. a B2C platform), you have to implement pagination to reduce the burden on the server and reduce latency and improve the app’s performance as well.

This article (which was recently updated to work well with Flutter 3 and newer) walks you through a complete example of creating a ListView with pagination in Flutter.

The Strategy

In the beginning, we send a get request and load only a fixed number of results (choose a number that makes sense in your case) and display them in a ListView.

When the user scrolls to the bottom of the ListView, we send another GET request and fetch another fixed number of results, and so on.

To know when the user scrolls near the bottom of a ListView, we will create a ScrollController and attach it to the ListView. When ScrollController.position.extendAfter has a value less than a certain level (200 or 300 is good to go), we will call a function that sends a new GET request.

You can find more information about ScrollController in the official docs. If you feel the words are too boring and confusing, just jump straight to the practical example below.

App Preview

This example will load some dummy blog posts from a public Pest API endpoint provided by jsonplaceholder.typicode.com (special thanks to the author for making an awesome tool for testing purposes):

https://jsonplaceholder.typicode.com/posts?_page=[page-number]&_limit=[some-number]

When the app launches for the first time, we will fetch the first 20 posts. Next, every time we scroll near the bottom of the ListView, a function named _loadMore will be called and this function will load 20 more posts.

After all the posts from the API have been loaded, a message will appear letting us know that even if we scroll down, nothing will happen.

Progress indicators will also appear while we are sending requests to the server.

A demo is worth more than thousands of words:

The Code

1. Create a new Flutter project.

2. To send http requests with ease, we will the http package. Install it by running:

dart pub add http

3. Remove all the default code in your main.dart and add the following:

// main.dart
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        // Remove the debug banner
        debugShowCheckedModeBanner: false,
        title: 'Kindacode.com',
        theme: ThemeData(
          primarySwatch: Colors.pink,
        ),
        home: const HomePage());
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  // We will fetch data from this Rest api
  final _baseUrl = 'https://jsonplaceholder.typicode.com/posts';

  // At the beginning, we fetch the first 20 posts
  int _page = 0;
  // you can change this value to fetch more or less posts per page (10, 15, 5, etc)
  final int _limit = 20;

  // There is next page or not
  bool _hasNextPage = true;

  // Used to display loading indicators when _firstLoad function is running
  bool _isFirstLoadRunning = false;

  // Used to display loading indicators when _loadMore function is running
  bool _isLoadMoreRunning = false;

  // This holds the posts fetched from the server
  List _posts = [];

  // This function will be called when the app launches (see the initState function)
  void _firstLoad() async {
    setState(() {
      _isFirstLoadRunning = true;
    });
    try {
      final res =
          await http.get(Uri.parse("$_baseUrl?_page=$_page&_limit=$_limit"));
      setState(() {
        _posts = json.decode(res.body);
      });
    } catch (err) {
      if (kDebugMode) {
        print('Something went wrong');
      }
    }

    setState(() {
      _isFirstLoadRunning = false;
    });
  }

  // This function will be triggered whenver the user scroll
  // to near the bottom of the list view
  void _loadMore() async {
    if (_hasNextPage == true &&
        _isFirstLoadRunning == false &&
        _isLoadMoreRunning == false &&
        _controller.position.extentAfter < 300) {
      setState(() {
        _isLoadMoreRunning = true; // Display a progress indicator at the bottom
      });
      _page += 1; // Increase _page by 1
      try {
        final res =
            await http.get(Uri.parse("$_baseUrl?_page=$_page&_limit=$_limit"));

        final List fetchedPosts = json.decode(res.body);
        if (fetchedPosts.isNotEmpty) {
          setState(() {
            _posts.addAll(fetchedPosts);
          });
        } else {
          // This means there is no more data
          // and therefore, we will not send another GET request
          setState(() {
            _hasNextPage = false;
          });
        }
      } catch (err) {
        if (kDebugMode) {
          print('Something went wrong!');
        }
      }

      setState(() {
        _isLoadMoreRunning = false;
      });
    }
  }

  // The controller for the ListView
  late ScrollController _controller;

  @override
  void initState() {
    super.initState();
    _firstLoad();
    _controller = ScrollController()..addListener(_loadMore);
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Kindacode.com'),
      ),
      body: _isFirstLoadRunning
          ? const Center(
              child: const CircularProgressIndicator(),
            )
          : Column(
              children: [
                Expanded(
                  child: ListView.builder(
                    controller: _controller,
                    itemCount: _posts.length,
                    itemBuilder: (_, index) => Card(
                      margin: const EdgeInsets.symmetric(
                          vertical: 8, horizontal: 10),
                      child: ListTile(
                        title: Text(_posts[index]['title']),
                        subtitle: Text(_posts[index]['body']),
                      ),
                    ),
                  ),
                ),

                // when the _loadMore function is running
                if (_isLoadMoreRunning == true)
                  const Padding(
                    padding: EdgeInsets.only(top: 10, bottom: 40),
                    child: Center(
                      child: CircularProgressIndicator(),
                    ),
                  ),

                // When nothing else to load
                if (_hasNextPage == false)
                  Container(
                    padding: const EdgeInsets.only(top: 30, bottom: 40),
                    color: Colors.amber,
                    child: const Center(
                      child: Text('You have fetched all of the content'),
                    ),
                  ),
              ],
            ),
    );
  }
}

Note: In many cases, the server side gives us information about the total number of results it has or the total number of pages it has. In these scenarios, you can still do the same as the example above or modify the code a bit to check whether _loadMore should be called or not.

Conclusion

We’ve examined an end-to-end example of implementing a ListView with pagination in a Flutter app. When you use your own API, there may be some differences in the URL structure, for example:

https://www.example.com/api/products?currentPage=1&perPage=10

Or:

https://www.kindacode.com/api/users?start=10&limit=20&order=desc

Make sure you understand the API you’re going to get data from. If not, talk to your backend developers and ask them to provide the necessary information.

If you’d like to learn more new stuff about Flutter, take a look at the following articles:

You can also take a tour around our Flutter topic page, or Dart topic page for the latest tutorials and examples.

Subscribe
Notify of
guest
7 Comments
Inline Feedbacks
View all comments
EDGAR NYAMBO
EDGAR NYAMBO
1 year ago

Thanks it helped

Ted
Ted
1 year ago

How would you use this in Firebase realtime database or some other database? Great tutorial!

Jeff Bezos
Jeff Bezos
1 year ago

Thank you for this

nairanitu
nairanitu
2 years ago

how to handle this using provider?

Ardy
Ardy
1 year ago
Reply to  A Goodman

do u already have provider sample for this case? please send ur link ..

Related Articles