infinite_scroll_pagination

repository·master·Indexed 20 days ago

https://github.com/edsonbueno/infinite_scroll_pagination

A Flutter package for lazily loading and displaying items as a user scrolls. It supports infinite scrolling, auto-pagination, and progressive loading through PagingController and Paged widgets such as PagedListView, PagedGridView, PagedSliverList, and PagedSliverGrid. The library is architecture-agnostic, allowing integration with any state management (e.g., BLoC, setState) and supports custom progress, error, and empty list indicators.

Tokens
12.5K
Snippets
30
Records
40
Agent score
72%

What's inside infinite_scroll_pagination

  1. Overview of Infinite Scroll Pagination features

    master

    The infinite_scroll_pagination package is designed to be unopinionated and highly customizable. Key characteristics include:

    • Architecture-agnostic: Works with any state management (setState, BLoC, etc.).
    • Layout-agnostic: Provides built-in widgets for GridView, SliverGrid, ListView, and SliverList (including .separated constructors), but allows for custom layouts.
    • API-agnostic: Supports any pagination strategy by giving you full control over API calls.
    • Highly customizable: Allows providing custom progress, error, and empty list indicators.
    • Extensible: Supports integration with pull-to-refresh, searching, filtering, and sorting.
    • State Listening: You can use a listener to react to state changes (e.g., showing snackbars or dialogs).
  2. Understand the PagingState structure in v5

    master

    The PagingState has been redesigned to be more flexible and supports direct extension. It no longer stores a single flat list of items or a single nextPageKey. Instead, it manages state through pages.

    Key Fields and Getters:

    • pages: A List<List<ItemType>> representing the items grouped by page.
    • items: An extension getter that flattens the pages into a single list.
    • keys: A list of all keys that have been fetched (one per page).
    • hasNextPage: A boolean indicating if more pages are available.
    • isLoading: A boolean indicating if a request is currently in progress.
    • error: An Object? representing the current error.
    • lastPageIsEmpty: A helper getter to check if the last fetched page was empty.
    • nextIntPageKey: A helper getter to retrieve the key for the next page.

    Data Manipulation: Because items are stored in pages, use the following extension methods to modify them:

    • mapItems: To modify items while retaining their page structure.
    • filterItems: To create locally filtered computed states.
  3. Manage pagination manually using setState

    master

    If you require more control over your state, you can bypass PagingController and manage PagingState manually using setState.

    To do this, you must:

    1. Maintain a PagingState<KeyType, ItemType> object in your state.
    2. Implement a fetch function that updates the state with new pages, keys, hasNextPage status, and isLoading/error flags.
    3. Pass the state and your fetch function directly to the PagedListView (or other Paged widgets).
    class _ExampleScreenState extends State<ExampleScreen> {
      PagingState<int, Photo> _state = PagingState();
    
      void _fetchNextPage() async {
        if (_state.isLoading) return;
    
        setState(() {
          _state = _state.copyWith(isLoading: true, error: null);
        });
    
        try {
          final newKey = (_state.keys?.last ?? 0) + 1;
          final newItems = await RemoteApi.getPhotos(newKey);
          final isLastPage = newItems.isEmpty;
    
          setState(() {
            _state = _state.copyWith(
              pages: [...?_state.pages, newItems],
              keys: [...?_state.keys, newKey],
              hasNextPage: !isLastPage,
              isLoading: false,
            );
          });
        } catch (error) {
          setState(() {
            _state = _state.copyWith(
              error: error,
              isLoading: false,
            );
          });
        }
      }
    
      @override
      Widget build(BuildContext context) => PagedListView<int, Photo>(
        state: _state,
        fetchNextPage: _fetchNextPage,
        builderDelegate: PagedChildBuilderDelegate(
          itemBuilder: (context, item, index) => ImageListTile(item: item),
        ),
      );
    }
  4. Implement Pull-to-Refresh

    master

    To add pull-to-refresh functionality, wrap your PagedListView, PagedGridView, or CustomScrollView with a standard Flutter RefreshIndicator. Inside the onRefresh callback, call the refresh() method on your PagingController.

    RefreshIndicator(
      onRefresh: () => Future.sync(
        () => _pagingController.refresh(),
      ),
      child: PagedListView<int, Photo>(
        state: state,
        fetchNextPage: fetchNextPage,
        builderDelegate: PagedChildBuilderDelegate(
          itemBuilder: (context, item, index) => ImageListTile(item: item),
        ),
      ),
    );
  5. Implement infinite scrolling pagination

    master

    To implement infinite scrolling, you need to use a PagingController to manage the pagination state and a PagedListView (or other Paged widgets) to display the items.

    1. Initialize a PagingController: Define the types for the page key (e.g., int) and the item type (e.g., Photo). Provide a getNextPageKey function to determine if more pages exist and a fetchPage function to perform the actual data fetching.
    2. Dispose the controller: Always call _pagingController.dispose() in your widget's dispose() method to prevent memory leaks.
    3. Use a PagingListener and PagedListView: Wrap your list in a PagingListener to provide the current state and the fetchNextPage callback to the PagedListView. Use a PagedChildBuilderDelegate to define how individual items are rendered via itemBuilder.
    class ListViewScreen extends StatefulWidget {
      const ListViewScreen({super.key});
    
      @override
      State<ListViewScreen> createState() => _ListViewScreenState();
    }
    
    class _ListViewScreenState extends State<ListViewScreen> {
      late final _pagingController = PagingController<int, Photo>(
        getNextPageKey: (state) => state.lastPageIsEmpty ? null : state.nextIntPageKey,
        fetchPage: (pageKey) => RemoteApi.getPhotos(pageKey),
      );
    
      @override
      void dispose() {
        _pagingController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) => PagingListener(
        controller: _pagingController,
        builder: (context, state, fetchNextPage) => PagedListView<int, Photo>(
          state: state,
          fetchNextPage: fetchNextPage,
          builderDelegate: PagedChildBuilderDelegate(
            itemBuilder: (context, item, index) => ImageListTile(item: item),
          ),
        ),
      );
  6. Integrate with custom state management (e.g., BLoC)

    master

    The package is agnostic to your state management choice. To use a custom solution like flutter_bloc, ensure your state holds a PagingState and your logic provides a way to trigger the next page fetch.

    Requirements for the Paged Widget:

    1. A PagingState object.
    2. A function to fetch the next page (e.g., a BLoC event trigger or a method on a ViewModel).

    In the example below, a BlocBuilder is used to rebuild the PagedListView whenever the PagingState inside the BLoC changes.

    // Inside your Bloc/Notifier
    // emit(state.copyWith(pages: [...?state.pages, newItems], ...));
    
    // In your Widget
    @override
    Widget build(BuildContext context) => BlocBuilder<PhotoBloc, PagingState<int, Photo>>(
      bloc: _bloc,
      builder: (context, state) => PagedListView<int, Photo>(
        state: state,
        fetchNextPage: _bloc.fetchNextPage,
        builderDelegate: PagedChildBuilderDelegate(
          itemBuilder: (context, item, index) => ImageListTile(item: item),
        ),
      ),
    );
  7. Customize the iOS Launch Screen assets

    master

    To customize the iOS launch screen, you can replace the existing image files in the example/ios/Runner/Assets.xcassets/LaunchImage.imageset/ directory with your own assets.

    Alternatively, you can manage these assets through Xcode:

    1. Open the iOS project workspace using open ios/Runner.xcworkspace.
    2. In the Xcode Project Navigator, navigate to Runner/Assets.xcassets.
    3. Drag and drop your desired images into the asset catalog.
    open ios/Runner.xcworkspace
  8. Use Sliver widgets for preceding or following items

    master

    If you need to include widgets that scroll along with the list (like headers, footers, or search bars), use the Sliver-based versions of the widgets: PagedSliverList and PagedSliverGrid. These must be placed inside a CustomScrollView.

    Note: Any preceding or following widgets must also be Slivers (e.g., using SliverToBoxAdapter for standard widgets).

    CustomScrollView(
      slivers: [
        SearchInputSliver(
          onChanged: updateSearchTerm,
        ),
        PagedSliverList<int, Photo>(
          state: state,
          fetchNextPage: fetchNextPage,
          builderDelegate: PagedChildBuilderDelegate(
            itemBuilder: (context, item, index) => ImageListTile(item: item),
          ),
        ),
      ],
    );
  9. Migrate from v4 to v5

    master

    In version 5, the package decouples PagingController from PagedLayoutBuilder and its descendants. This allows for greater freedom in managing PagingState and makes it easier to integrate with various state management solutions. This is a breaking change that requires refactoring how controllers and layouts are connected.

    Key architectural shifts:

    • PagingController is now optional.
    • PagedLayoutBuilder (and subclasses like PagedListView) now accepts a PagingState and a fetchNextPage function instead of a controller.
    • PagingState has been restructured to store items as a list of pages (List<List<ItemType>>) rather than a flat list.
    // v4 pattern (Deprecated/Removed)
    PagedListView.builder(
      pagingController: pagingController,
      builderDelegate: PagedChildBuilderDelegate(
        itemBuilder: (context, item, index) => ImageListTile(item),
      ),
    )
    
    // v5 pattern (New)
    PagedListView.builder(
      state: state,
      fetchNextPage: fetchNextPage,
      builderDelegate: PagedChildBuilderDelegate(
        itemBuilder: (context, item, index) => ImageListTile(item),
      ),
    )
  10. Manage pagination with PagingController

    master

    The PagingController is the built-in solution for managing PagingState. It handles page keys and data fetching. To use it with a Paged Widget, wrap the widget in a PagingListener which provides the current state and a fetchNextPage callback.

    Key responsibilities:

    • getNextPageKey: A function that determines the next key based on the current state (e.g., returning null when the last page is reached).
    • fetchPage: A function that performs the actual data fetching using a provided pageKey.

    Always call dispose() on your PagingController when the controller is no longer needed to prevent memory leaks.

    class _ExampleScreenState extends State<ExampleScreen> {
      late final _pagingController = PagingController<int, Photo>(
        getNextPageKey: (state) => state.lastPageIsEmpty ? null : state.nextIntPageKey,
        fetchPage: (pageKey) => RemoteApi.getPhotos(pageKey),
      );
    
      @override
      void dispose() {
        _pagingController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) => PagingListener(
        controller: _pagingController,
        builder: (context, state, fetchNextPage) => PagedListView<int, Photo>(
          state: state,
          fetchNextPage: fetchNextPage,
          builderDelegate: PagedChildBuilderDelegate(
            itemBuilder: (context, item, index) => ImageListTile(item: item),
          ),
        ),
      );
    }