paginated_bloc_widget 1.1.0 copy "paginated_bloc_widget: ^1.1.0" to clipboard
paginated_bloc_widget: ^1.1.0 copied to clipboard

A production-ready Flutter pagination widget using BLoC, with built-in loading, error handling, pull-to-refresh, and support for all major scroll views.

Paginated BLoC Widget #

pub package License: MIT

A powerful, flexible, and production-ready pagination widget for Flutter using the BLoC pattern. This package provides a complete solution for implementing infinite scroll pagination with support for ListView, GridView, PageView, CustomScrollView, and Slivers.

โœจ Features #

  • ๐ŸŽฏ Generic Type Support - Works with any data model
  • ๐Ÿ“ฑ Multiple Layout Types - ListView, GridView, PageView, Slivers
  • ๐Ÿ”„ Built-in States - Loading, error, empty, and success states
  • โ™ป๏ธ Pull-to-Refresh - Native refresh indicator support
  • ๐Ÿ“ Customizable Threshold - Configure when to trigger load more
  • ๐ŸŽจ Fully Customizable - Override any widget state
  • ๐Ÿงช Testable - Includes in-memory repository for testing
  • ๐Ÿ“ฆ Zero Dependencies - Only relies on flutter_bloc and equatable

๐Ÿ“ฆ Installation #

Add this to your pubspec.yaml:

dependencies:
  paginated_bloc_widget: ^1.1.0

Then run:

flutter pub get

๐Ÿš€ Quick Start #

1. Create Your Model #

class User {
  final int id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});

  factory User.fromJson(Map<String, dynamic> json) => User(
    id: json['id'],
    name: json['name'],
    email: json['email'],
  );
}

2. Implement the Repository #

import 'package:paginated_bloc_widget/paginated_bloc_widget.dart';

class UserRepository extends PaginatedDataRepository<User> {
  final ApiClient _client;

  UserRepository(this._client);

  @override
  Future<PaginatedResponse<User>> fetchData({
    required int page,
    int limit = 10,
    Map<String, dynamic>? filters,
  }) async {
    final response = await _client.getUsers(page: page, limit: limit);
    
    return PaginatedResponse(
      data: response.users.map((e) => User.fromJson(e)).toList(),
      hasMore: page < response.totalPages,
      currentPage: page,
      totalPages: response.totalPages,
      totalItems: response.totalCount,
    );
  }
}

3. Use the Widget #

import 'package:paginated_bloc_widget/paginated_bloc_widget.dart';

class UserListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => PaginatedDataBloc<User>(
        repository: UserRepository(context.read<ApiClient>()),
        itemsPerPage: 20,
      )..add(const LoadFirstPage()),
      child: Scaffold(
        appBar: AppBar(title: const Text('Users')),
        body: PaginatedDataWidget<User>(
          enablePullToRefresh: true,
          itemBuilder: (context, user, index) => ListTile(
            leading: CircleAvatar(child: Text(user.name[0])),
            title: Text(user.name),
            subtitle: Text(user.email),
          ),
        ),
      ),
    );
  }
}

๐Ÿ“– Usage Examples #

ListView with Separator #

PaginatedDataWidget<User>(
  layoutType: PaginatedLayoutType.listView,
  separatorWidget: const Divider(height: 1),
  enablePullToRefresh: true,
  itemBuilder: (context, user, index) => ListTile(
    title: Text(user.name),
  ),
)

GridView #

PaginatedDataWidget<Product>(
  layoutType: PaginatedLayoutType.gridView,
  crossAxisCount: 2,
  childAspectRatio: 0.75,
  crossAxisSpacing: 8,
  mainAxisSpacing: 8,
  padding: const EdgeInsets.all(16),
  itemBuilder: (context, product, index) => ProductCard(product: product),
)

Horizontal PageView #

PaginatedDataWidget<Story>(
  layoutType: PaginatedLayoutType.pageView,
  scrollDirection: ScrollDirection.horizontal,
  enablePageSnapping: true,
  itemBuilder: (context, story, index) => StoryPage(story: story),
)

CustomScrollView with Sliver Headers #

PaginatedDataWidget<Item>(
  layoutType: PaginatedLayoutType.sliverList,
  sliverHeaders: [
    SliverAppBar(
      title: const Text('My Items'),
      floating: true,
    ),
    SliverToBoxAdapter(
      child: Container(
        height: 100,
        child: const Text('Header Content'),
      ),
    ),
  ],
  itemBuilder: (context, item, index) => ItemTile(item: item),
)

Custom State Widgets (Local Override) #

Override state widgets for a specific widget (highest priority over global theme):

PaginatedDataWidget<User>(
  firstPageLoadingWidget: const ShimmerList(),
  loadMoreLoadingWidget: const SmallLoader(),
  emptyWidget: const EmptyState(
    icon: Icons.people_outline,
    message: 'No users found',
  ),
  firstPageErrorWidget: (error, retry) => ErrorWidget(
    message: error,
    onRetry: retry,
  ),
  loadMoreErrorWidget: (error, retry) => TextButton(
    onPressed: retry,
    child: Text('Error: $error. Tap to retry'),
  ),
  itemBuilder: (context, user, index) => UserTile(user: user),
)

Tip: For app-wide consistent styling, use PaginationTheme instead of repeating customizations in every widget.

With Filters #

PaginatedDataBloc<User>(
  repository: userRepository,
  filters: {
    'status': 'active',
    'role': 'admin',
    'sortBy': 'createdAt',
  },
)..add(const LoadFirstPage())

๐Ÿ”ง BLoC Events #

Event Description
LoadFirstPage() Load the first page of data
LoadMoreData() Load the next page
RefreshData() Refresh and reload first page
ResetPagination() Reset to initial state
UpdateItem<T>(item, matcher) Update an existing item
RemoveItem<T>(item, matcher) Remove an item from the list
AddItem<T>(item, insertAtStart) Add a new item

Updating Items #

// Update a user in the list
context.read<PaginatedDataBloc<User>>().add(
  UpdateItem<User>(
    updatedUser,
    matcher: (oldItem, newItem) => oldItem.id == newItem.id,
  ),
);

Removing Items #

// Remove a user by ID
context.read<PaginatedDataBloc<User>>().add(
  RemoveItem<User>(
    matcher: (item) => item.id == deletedUserId,
  ),
);

Adding Items #

// Add a new user at the start
context.read<PaginatedDataBloc<User>>().add(
  AddItem<User>(newUser, insertAtStart: true),
);

๐Ÿ“Š State Properties #

Access state properties in your UI:

BlocBuilder<PaginatedDataBloc<User>, PaginatedDataState<User>>(
  builder: (context, state) {
    // Helper getters
    state.isInitial          // Initial state
    state.isFirstPageLoading // Loading first page
    state.isLoadingMore      // Loading more items
    state.isRefreshing       // Refreshing data
    state.hasError           // Any error occurred
    state.isEmpty            // No items loaded
    state.isSuccess          // Data loaded successfully
    
    // Data access
    state.items              // List of loaded items
    state.itemCount          // Number of items
    state.currentPage        // Current page number
    state.hasReachedMax      // All pages loaded
    state.totalItems         // Total items (if known)
    state.totalPages         // Total pages (if known)
    state.loadProgress       // Loading progress (0.0 - 1.0)
    state.error              // Error message
    
    return YourWidget();
  },
)

๐Ÿงช Testing #

Use the included InMemoryPaginatedRepository for testing:

final testRepository = InMemoryPaginatedRepository<User>(
  items: List.generate(100, (i) => User(id: i, name: 'User $i')),
  simulatedDelay: const Duration(milliseconds: 500),
);

final bloc = PaginatedDataBloc<User>(
  repository: testRepository,
  itemsPerPage: 10,
);

โš™๏ธ Configuration #

Global Configuration with PaginationConfig #

Set global defaults for all pagination widgets at app startup:

void main() {
  // Initialize global pagination settings
  PaginationConfig.init(
    itemsPerPage: 20,              // Default items per page
    loadMoreThreshold: 0.85,       // Scroll threshold (0.0 to 1.0)
    pageViewLoadMoreOffset: 2,     // Pages from end to load more in PageView
  );
  
  runApp(const MyApp());
}

PaginationConfig Defaults:

Setting Default Description
itemsPerPage 10 Number of items to fetch per page
loadMoreThreshold 0.8 Scroll position threshold (80% scrolled)
pageViewLoadMoreOffset 3 Pages from end to trigger load in PageView

Global Widget Theming with PaginationTheme #

Provide custom widget builders for all paginated widgets in your app:

@override
Widget build(BuildContext context) {
  return PaginationTheme(
    data: PaginationThemeData(
      firstPageLoadingBuilder: (context) => const Center(
        child: CircularProgressIndicator.adaptive(),
      ),
      loadMoreLoadingBuilder: (context) => const Padding(
        padding: EdgeInsets.all(16),
        child: Center(child: CircularProgressIndicator()),
      ),
      firstPageErrorBuilder: (context, error, retry) => Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.error_outline, size: 64, color: Colors.red),
            const SizedBox(height: 16),
            Text(error, textAlign: TextAlign.center),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: retry,
              child: const Text('Retry'),
            ),
          ],
        ),
      ),
      loadMoreErrorBuilder: (context, error, retry) => Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Error: $error'),
            const SizedBox(width: 8),
            ElevatedButton(
              onPressed: retry,
              child: const Text('Retry'),
            ),
          ],
        ),
      ),
      emptyBuilder: (context) => const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.inbox_outlined, size: 64, color: Colors.grey),
            SizedBox(height: 16),
            Text('No items found'),
          ],
        ),
      ),
    ),
    child: MaterialApp(
      home: MyHomePage(),
    ),
  );
}

Local Widget Override #

Override global theme for specific widgets (highest priority):

PaginatedDataWidget<User>(
  // These override the global PaginationTheme
  firstPageLoadingWidget: const CustomLoader(),
  loadMoreLoadingWidget: const SmallSpinner(),
  emptyWidget: const CustomEmptyState(),
  firstPageErrorWidget: (error, retry) => CustomErrorWidget(
    message: error,
    onRetry: retry,
  ),
  itemBuilder: (context, user, index) => UserTile(user: user),
)

Resolution Order:

  1. Local widget parameter (passed to PaginatedDataWidget)
  2. Global PaginationTheme data
  3. Default built-in widget

Complete Setup Example #

void main() {
  // Step 1: Set global pagination config
  PaginationConfig.init(
    itemsPerPage: 15,
    loadMoreThreshold: 0.9,
  );
  
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return PaginationTheme(
      // Step 2: Set global theme for all widgets
      data: PaginationThemeData(
        firstPageLoadingBuilder: (context) => const CustomAppLoader(),
        emptyBuilder: (context) => const CustomEmptyState(),
      ),
      child: MaterialApp(
        home: UserListPage(),
      ),
    );
  }
}

class UserListPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => PaginatedDataBloc<User>(
        repository: UserRepository(),
        itemsPerPage: 20, // Override global config (10 โ†’ 20)
      )..add(const LoadFirstPage()),
      child: Scaffold(
        appBar: AppBar(title: const Text('Users')),
        // Step 3: Optional - override theme for this widget only
        body: PaginatedDataWidget<User>(
          firstPageLoadingWidget: const LinearProgressIndicator(), // Override theme
          itemBuilder: (context, user, index) => UserTile(user: user),
        ),
      ),
    );
  }
}

Widget Properties #

Property Type Default Description
layoutType PaginatedLayoutType listView Layout type to use
scrollDirection ScrollDirection vertical Scroll direction
loadMoreThreshold double? Global value Scroll threshold to trigger load more
enablePullToRefresh bool false Enable pull-to-refresh
shrinkWrap bool false Shrink wrap content
crossAxisCount int? 2 Grid columns
childAspectRatio double? 1.0 Grid item aspect ratio

๐Ÿ“ License #

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿค Contributing #

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

๐Ÿ“ฃ Support #

If you find this package helpful, please give it a โญ on GitHub!

For bugs and feature requests, please open an issue.

3
likes
160
points
68
downloads

Publisher

unverified uploader

Weekly Downloads

A production-ready Flutter pagination widget using BLoC, with built-in loading, error handling, pull-to-refresh, and support for all major scroll views.

Repository (GitHub)
View/report issues

Topics

#pagination #bloc #infinite-scroll #listview #gridview

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

bloc, equatable, flutter, flutter_bloc

More

Packages that depend on paginated_bloc_widget