DEV Community

Cover image for Pagination in flutter
niranjantk
niranjantk

Posted on

Pagination in flutter

Pagination in Flutter Step by Step

Pagination is the process of loading data in chunks, rather than fetching the full dataset at once. This is essential for performance, especially when dealing with large datasets.


Key Terms

Term Meaning
Page Current page number being loaded (usually starts at 1)
Page Size Number of items per request
Total Pages Total pages returned by API
hasNextPage Whether there are more pages to load
loading / moreLoading Flags to show progress indicators

Common Pattern

  1. Load the first page when the screen opens.
  2. Listen to scroll events.
  3. When the user reaches the bottom, fetch the next page if it exists.
  4. Append new data to the existing list.
  5. Show a loading spinner at the bottom while fetching.

Provider Setup for Pagination

class PaginationProvider extends ChangeNotifier {
  List<String> items = []; // Your list of items
  int page = 1;
  int pageSize = 10;
  int totalPages = 1;
  bool moreLoading = false;
  bool initialLoading = true;

  // Reset pagination (call this when opening page)
  void reset() {
    items.clear();
    page = 1;
    totalPages = 1;
    moreLoading = false;
    initialLoading = true;
    notifyListeners();
  }

  // Load first page
  Future<void> loadInitialData() async {
    reset();
    await loadMore();
    initialLoading = false;
    notifyListeners();
  }

  // Load next page
  Future<void> loadMore() async {
    if (moreLoading || page > totalPages) return;

    moreLoading = true;
    notifyListeners();

    try {
      // Simulate API call
      final response = await fakeApiCall(page, pageSize);

      items.addAll(response['data']);
      totalPages = response['totalPages'];
      page += 1;
    } catch (e) {
      print("Error fetching page $page: $e");
    }

    moreLoading = false;
    notifyListeners();
  }

  // Fake API call for demonstration
  Future<Map<String, dynamic>> fakeApiCall(int page, int pageSize) async {
    await Future.delayed(Duration(seconds: 1)); // simulate network delay
    int totalItems = 42; // total items in backend
    int totalPages = (totalItems / pageSize).ceil();
    List<String> data = List.generate(
        pageSize, (i) => "Item ${(page - 1) * pageSize + i + 1}")
        .where((item) => int.parse(item.split(" ").last) <= totalItems)
        .toList();

    return {'data': data, 'totalPages': totalPages};
  }
}
Enter fullscreen mode Exit fullscreen mode

UI Implementation

class PaginationPage extends StatefulWidget {
  @override
  _PaginationPageState createState() => _PaginationPageState();
}

class _PaginationPageState extends State<PaginationPage> {
  final ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    final provider = Provider.of<PaginationProvider>(context, listen: false);
    provider.loadInitialData();

    _scrollController.addListener(() {
      final provider = Provider.of<PaginationProvider>(context, listen: false);

      // Load next page when near bottom
      if (_scrollController.position.pixels >=
          _scrollController.position.maxScrollExtent - 100) {
        provider.loadMore();
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Consumer<PaginationProvider>(
      builder: (context, provider, child) {
        if (provider.initialLoading) {
          return Center(child: CircularProgressIndicator());
        }

        if (provider.items.isEmpty) {
          return Center(child: Text("No items found"));
        }

        return ListView.builder(
          controller: _scrollController,
          itemCount: provider.items.length + 1, // extra item for loader
          itemBuilder: (context, index) {
            if (index == provider.items.length) {
              // Show loading indicator at bottom
              return provider.moreLoading
                  ? Padding(
                      padding: const EdgeInsets.all(16),
                      child: Center(child: CircularProgressIndicator()),
                    )
                  : SizedBox();
            }

            return ListTile(title: Text(provider.items[index]));
          },
        );
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Best Practices

  • Reset pagination when page opens.
    Avoid calling reset in the scroll listener — only in initState.

  • Use flags

    • initialLoading → for the first page.
    • moreLoading → for loading next pages.
  • Always check page <= totalPages before calling loadMore.

  • Add bottom padding for smooth scrolling and avoid triggering too early.

  • Optional: Debounce scroll events for very fast scrolling.


Flow Recap

  1. Screen opens → loadInitialData() → fetch first page.
  2. User scrolls → scroll listener triggers → check if near bottom → loadMore().
  3. Append new items → notifyListeners() → rebuild list.
  4. Loader shows while fetching next page.

Top comments (0)