riverpod_paging_utils 1.0.0 copy "riverpod_paging_utils: ^1.0.0" to clipboard
riverpod_paging_utils: ^1.0.0 copied to clipboard

Flutter/Riverpod pagination utilities. Easily build screens with loading/error states. Supports page, offset, and cursor-based pagination.

example/lib/main.dart

import 'package:example/data/sample_item.dart';
import 'package:example/repository/sample_repository.dart';
import 'package:example/ui/custom_scroll_view_screen.dart';
import 'package:example/ui/first_page_error_screen.dart';
import 'package:example/ui/gridview_screen.dart';
import 'package:example/ui/paging_method_screen.dart';
import 'package:example/ui/passing_args_screen.dart';
import 'package:example/ui/second_page_error_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';

part 'main.g.dart';

void main() {
  runApp(
    ProviderScope(
      // Disable automatic retry for testing error screens
      retry: (retryCount, error) => null,
      child: const MainApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        extensions: [
          PagingHelperViewTheme(
            // Custom error view with Semantics identifier for E2E testing
            errorViewBuilder:
                (context, error, stackTrace, onRefreshPressed) => Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Semantics(
                        identifier: 'error-view',
                        child: Column(
                          children: [
                            const Icon(Icons.error_outline, size: 48),
                            const SizedBox(height: 16),
                            Text('Error: $error'),
                          ],
                        ),
                      ),
                      const SizedBox(height: 16),
                      Semantics(
                        identifier: 'retry-button',
                        button: true,
                        container: true,
                        child: GestureDetector(
                          onTap: onRefreshPressed,
                          child: Container(
                            padding: const EdgeInsets.symmetric(
                              horizontal: 24,
                              vertical: 12,
                            ),
                            decoration: BoxDecoration(
                              color: Theme.of(context).colorScheme.primary,
                              borderRadius: BorderRadius.circular(20),
                            ),
                            child: Text(
                              'Retry',
                              style: TextStyle(
                                color: Theme.of(context).colorScheme.onPrimary,
                              ),
                            ),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
            // Custom end error view with Semantics identifier for E2E testing
            endErrorViewBuilder:
                (context, error, onRetryPressed) => Center(
                  child: Padding(
                    padding: const EdgeInsets.all(16),
                    child: Column(
                      children: [
                        Semantics(
                          identifier: 'end-error-view',
                          child: Text('$error'),
                        ),
                        const SizedBox(height: 8),
                        Semantics(
                          identifier: 'error-retry-button',
                          button: true,
                          container: true,
                          child: GestureDetector(
                            onTap: onRetryPressed,
                            child: Container(
                              padding: const EdgeInsets.symmetric(
                                horizontal: 24,
                                vertical: 12,
                              ),
                              decoration: BoxDecoration(
                                color: Theme.of(context).colorScheme.primary,
                                borderRadius: BorderRadius.circular(20),
                              ),
                              child: Text(
                                'Retry',
                                style: TextStyle(
                                  color:
                                      Theme.of(context).colorScheme.onPrimary,
                                ),
                              ),
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
          ),
        ],
      ),
      home: const SampleScreen(),
    );
  }
}

/// A Riverpod provider that mixes in [CursorPagingNotifierMixin].
/// This provider handles the pagination logic for fetching [SampleItem] data using cursor-based pagination.
@riverpod
class SampleNotifier extends _$SampleNotifier
    with CursorPagingNotifierMixin<SampleItem> {
  /// Builds the initial state of the provider by fetching data with a null cursor.
  @override
  Future<CursorPagingData<SampleItem>> build() => fetch(cursor: null);

  /// Fetches paginated data from the [SampleRepository] based on the provided [cursor].
  /// Returns a [CursorPagingData] object containing the fetched items, a flag indicating whether more data is available,
  /// and the next cursor for fetching the next page.
  @override
  Future<CursorPagingData<SampleItem>> fetch({required String? cursor}) async {
    final repository = ref.read(sampleRepositoryProvider);
    final (items, nextCursor) = await repository.getByCursor(cursor);
    final hasMore = nextCursor != null && nextCursor.isNotEmpty;

    return CursorPagingData(
      items: items,
      hasMore: hasMore,
      nextCursor: nextCursor,
    );
  }
}

/// A sample page that demonstrates the usage of [PagingHelperView] with the [SampleNotifier] provider.
class SampleScreen extends StatelessWidget {
  const SampleScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: Builder(
          builder:
              (context) => Semantics(
                identifier: 'drawer-menu-button',
                child: IconButton(
                  icon: const Icon(Icons.menu),
                  onPressed: () => Scaffold.of(context).openDrawer(),
                ),
              ),
        ),
        title: const Text('Sample Screen'),
      ),
      drawer: Drawer(
        child: ListView(
          children: [
            const DrawerHeader(child: Text('Menu')),
            ListTile(
              title: const Text('1st page error'),
              onTap:
                  () async =>
                      Navigator.of(context).push(FirstPageErrorScreen.route()),
            ),
            ListTile(
              title: const Text('2nd page error'),
              onTap:
                  () async =>
                      Navigator.of(context).push(SecondPageErrorScreen.route()),
            ),
            ListTile(
              title: const Text('Passing args screen'),
              onTap:
                  () async =>
                      Navigator.of(context).push(PassingArgsScreen.route()),
            ),
            ListTile(
              title: const Text('Paging method screen'),
              onTap:
                  () async =>
                      Navigator.of(context).push(PagingMethodScreen.route()),
            ),
            ListTile(
              title: const Text('GridView example'),
              onTap:
                  () async =>
                      Navigator.of(context).push(GridViewScreen.route()),
            ),
            ListTile(
              title: const Text('CustomScrollView example'),
              onTap:
                  () async => Navigator.of(
                    context,
                  ).push(CustomScrollViewScreen.route()),
            ),
          ],
        ),
      ),
      body: PagingHelperView(
        provider: sampleProvider,
        futureRefreshable: sampleProvider.future,
        notifierRefreshable: sampleProvider.notifier,
        contentBuilder:
            (data, widgetCount, endItemView) => ListView.builder(
              itemCount: widgetCount,
              itemBuilder: (context, index) {
                // if the index is last, then
                // return the end item view.
                if (index == widgetCount - 1) {
                  return endItemView;
                }

                // Otherwise, build a list tile for each sample item.
                return Semantics(
                  identifier: 'sample-item-$index',
                  child: ListTile(
                    key: ValueKey(data.items[index].id),
                    title: Text(data.items[index].name),
                    subtitle: Text(data.items[index].id),
                  ),
                );
              },
            ),
      ),
    );
  }
}
12
likes
160
points
243
downloads

Publisher

verified publisherk9i.app

Weekly Downloads

Flutter/Riverpod pagination utilities. Easily build screens with loading/error states. Supports page, offset, and cursor-based pagination.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, flutter_riverpod, freezed_annotation, visibility_detector

More

Packages that depend on riverpod_paging_utils