DEV Community

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.


1️⃣ 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

2️⃣ 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.

3️⃣ 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

4️⃣ 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

5️⃣ 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.


6️⃣ 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.

This is a complete and reusable pagination template. You can adapt it for discussions, comments, videos, or any API-driven list.


If you want, I can also make a “Production-ready” version with API integration, error handling, and pull-to-refresh so it’s ready for real apps.

Do you want me to do that next?

Top comments (0)