scrolled_pagination 1.4.1 copy "scrolled_pagination: ^1.4.1" to clipboard
scrolled_pagination: ^1.4.1 copied to clipboard

A reusable MVC-style infinite-scroll pagination toolkit for Flutter with multiple providers (Future/Stream/sync), page or cursor-based pagination, reach-end state, item operations, batch transactions, [...]

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:scrolled_pagination/scrolled_pagination.dart';

import 'chrome.dart';
import 'code_reference.dart';
import 'data.dart';
import 'frames.dart';
import 'theme.dart';
import 'widgets.dart';

void main() => runApp(const DemoApp());

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'scrolled_pagination',
      debugShowCheckedModeBanner: false,
      theme: CA.theme(),
      home: const Workbench(),
    );
  }
}

class Workbench extends StatefulWidget {
  const Workbench({super.key});
  @override
  State<Workbench> createState() => _WorkbenchState();
}

class _WorkbenchState extends State<Workbench> {
  String dataset = 'feed';
  String mode = 'page';
  String kind = 'future';
  String layout = 'list';
  String sortMode = 'none';
  bool emptyOn = false;
  bool reachEndOn = true;
  String lastEvent = '—';

  SortManager<Object>? _sort;

  static const double _canvasWidth = 1460;

  late PaginationController<Object> _controller = _make();

  int _keyOf(Object o) => o is Post ? o.id : (o as Member).id;

  PaginationController<Object> _make() {
    _sort = switch (sortMode) {
      'asc' => SortManager<Object>.byIndex(_keyOf, enabled: true, order: SortOrder.ascending),
      'desc' => SortManager<Object>.byIndex(_keyOf, enabled: true, order: SortOrder.descending),
      _ => null,
    };
    return PaginationController<Object>(
      pageSize: 12,
      initialPageParam: mode == 'cursor' ? null : 1,
      getKey: _keyOf,
      openFocusManager: true,
      sortManager: _sort,
      provider: makeProvider(dataset: dataset, mode: mode, kind: kind),
      // Reach-end is inferred from the page's hasMore flag / a short page.
      // To override, pass: detectReachEnd: (page, items) => page.items.isEmpty
      onItemInserted: (i, _) => _log('onItemInserted #$i'),
      onItemUpdated: (i, _) => _log('onItemUpdated #$i'),
      onItemDeleted: (i, _) => _log('onItemDeleted #$i'),
      onItemReplaced: (i, _) => _log('onItemReplaced #$i'),
      onItemMoved: (a, b) => _log('onItemMoved $a→$b'),
      onFocusChanged: (i, _) => _log('onFocusChanged #$i'),
      onJumbFailed: (f) => _log('onJumbFailed: ${f.reason}'),
      onIndexOutOfRange: (i) => _log('onIndexOutOfRange #$i'),
    );
  }

  void _log(String e) => setState(() => lastEvent = e);

  void _reconfigure() {
    final old = _controller;
    _controller = _make();
    setState(() {});
    WidgetsBinding.instance.addPostFrameCallback((_) => old.dispose());
  }

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

  Object? get _first => _controller.items.isEmpty ? null : _controller.items.first;

  Object _synthetic({String? title}) {
    final id = DateTime.now().microsecondsSinceEpoch;
    if (dataset == 'members') {
      return Member(id: id, name: 'New Member', role: 'Member', club: 'Madina IT', online: true, tasks: 0, since: 2026);
    }
    return Post(
      id: id,
      kind: 'news',
      club: 'Madina IT',
      author: 'You',
      when: 'Just now',
      title: title ?? 'New post added via controller',
      excerpt: 'This item was inserted without rebuilding the rest of the list.',
      likes: 0,
      comments: 0,
    );
  }

  PaginationLayout get _layout => switch (layout) {
        'grid' => PaginationLayout.grid,
        'separated' => PaginationLayout.separated,
        _ => PaginationLayout.list,
      };
  bool get _reverse => layout == 'reverse';
  bool get _grid => layout == 'grid';

  // --- item builders ----------------------------------------------------------
  Widget _phoneItem(BuildContext context, Object item, int index) {
    if (item is Member) return _grid ? MemberCardW(member: item) : MemberRowW(member: item);
    final p = item as Post;
    return _grid ? FeedCardV(post: p) : FeedCardH(post: p);
  }

  Widget _browserItem(BuildContext context, Object item, int index) {
    if (item is Member) return _grid ? MemberCardW(member: item) : MemberRowW(member: item);
    final p = item as Post;
    return _grid ? FeedCardV(post: p) : FeedRowWide(post: p);
  }

  // --- shared builder slots ---------------------------------------------------
  Widget _firstLoading(BuildContext _) => SkeletonList(grid: _grid);
  Widget _empty(BuildContext _, Future<void> Function() refresh) => _Empty(onRefresh: refresh, dataset: dataset);
  Widget _error(BuildContext _, Object error, Future<void> Function() retry) => _Error(error: error, onRetry: retry);

  PaginatedScrollView<Object> _view({
    required String tag,
    required PaginatedItemBuilder<Object> itemBuilder,
    required EdgeInsets padding,
    required double gap,
    required int crossAxisCount,
    required double childAspectRatio,
    required bool pullToRefresh,
  }) {
    return PaginatedScrollView<Object>(
      key: ValueKey('$tag|$dataset|$mode|$kind|$layout'),
      controller: _controller,
      layout: _layout,
      reverse: _reverse,
      loadMoreThreshold: 280,
      padding: padding,
      gap: gap,
      crossAxisCount: crossAxisCount,
      childAspectRatio: childAspectRatio,
      pullToRefresh: pullToRefresh,
      itemBuilder: itemBuilder,
      separatorBuilder: (_, __) => const Divider(height: 20, color: CA.ink100),
      firstPageLoadingBuilder: _firstLoading,
      loadMoreBuilder: (_) => const _LoadMore(),
      // reachEndBuilder (optional) wins over noMoreBuilder when toggled on;
      // pass null and nothing renders at the end.
      reachEndBuilder: reachEndOn ? (_) => const _ReachEnd() : null,
      noMoreBuilder: (_) => const _NoMore(),
      emptyBuilder: _empty,
      errorBuilder: _error,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: CA.canvas,
      body: SafeArea(
        child: SingleChildScrollView(
          child: SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            child: Padding(
              padding: const EdgeInsets.fromLTRB(24, 20, 24, 32),
              child: SizedBox(
                width: _canvasWidth,
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    _header(),
                    const SizedBox(height: 16),
                    _controls(),
                    const SizedBox(height: 16),
                    _actions(),
                    const SizedBox(height: 18),
                    _stage(),
                    const SizedBox(height: 16),
                    const StatesLegend(),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }

  // --- header -----------------------------------------------------------------
  Widget _header() => Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Container(
            width: 46,
            height: 46,
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(13),
              gradient: const LinearGradient(
                  colors: [CA.violet, CA.violet700],
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight),
            ),
            alignment: Alignment.center,
            child: const Text('SP', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 18)),
          ),
          const SizedBox(width: 14),
          const Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: [
                Text('Scrolled Pagination',
                    style: TextStyle(fontSize: 23, fontWeight: FontWeight.w700, color: CA.ink900)),
                SizedBox(height: 2),
                Text('One controller · many providers · eight builders — a reusable infinite-scroll component for ClubApp',
                    style: TextStyle(fontSize: 13, color: CA.ink500)),
              ],
            ),
          ),
          const SizedBox(width: 24),
          StatusPill(controller: _controller, lastEvent: lastEvent),
        ],
      );

  // --- segmented controls -----------------------------------------------------
  Widget _divider() => Container(width: 1, color: CA.ink100, margin: const EdgeInsets.symmetric(vertical: 2));

  Widget _controls() => Container(
        padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
        decoration: BoxDecoration(
          color: CA.surface,
          borderRadius: BorderRadius.circular(16),
          boxShadow: CA.cardShadow,
        ),
        child: IntrinsicHeight(
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Segmented<String>(
                label: 'Data source',
                value: dataset,
                options: const [(value: 'feed', label: 'Feed posts'), (value: 'members', label: 'Members')],
                onChanged: (v) { dataset = v; _reconfigure(); },
              ),
              const SizedBox(width: 16),
              _divider(),
              const SizedBox(width: 16),
              Segmented<String>(
                label: 'Pagination',
                value: mode,
                options: const [(value: 'page', label: 'Page-based'), (value: 'cursor', label: 'Cursor-based')],
                onChanged: (v) { mode = v; _reconfigure(); },
              ),
              const SizedBox(width: 16),
              _divider(),
              const SizedBox(width: 16),
              Segmented<String>(
                label: 'Provider',
                value: kind,
                options: const [(value: 'future', label: 'Future'), (value: 'stream', label: 'Stream'), (value: 'sync', label: 'Sync')],
                onChanged: (v) { kind = v; _reconfigure(); },
              ),
              const SizedBox(width: 16),
              _divider(),
              const SizedBox(width: 16),
              Segmented<String>(
                label: 'Layout',
                value: layout,
                options: const [(value: 'list', label: 'List'), (value: 'grid', label: 'Grid'), (value: 'separated', label: 'Separated'), (value: 'reverse', label: 'Reverse')],
                onChanged: (v) => setState(() => layout = v),
              ),
              const SizedBox(width: 16),
              _divider(),
              const SizedBox(width: 16),
              Segmented<String>(
                label: 'Sort',
                value: sortMode,
                options: const [(value: 'none', label: 'Insertion'), (value: 'asc', label: 'Id ↑'), (value: 'desc', label: 'Id ↓')],
                onChanged: (v) { sortMode = v; _reconfigure(); },
              ),
            ],
          ),
        ),
      );

  // --- action rows ------------------------------------------------------------
  Object? _at(int i) => (i >= 0 && i < _controller.count) ? _controller.items[i] : null;

  Widget _actions() => Column(
        children: [
          ActionRow(label: 'Loading', children: [
            ActionButton('Refresh', tone: ActTone.accent, icon: Icons.refresh, onTap: _controller.refresh),
            ActionButton('Load more', onTap: _controller.loadMore),
            ActionButton('Retry', onTap: _controller.retry),
            ActionButton('Force error', tone: ActTone.danger, onTap: () {
              Sim.failRate = 1;
              _controller.refresh().whenComplete(() => Sim.failRate = 0);
            }),
            ActionButton(emptyOn ? 'Empty: on' : 'Empty state', tone: emptyOn ? ActTone.danger : ActTone.normal, onTap: () {
              setState(() => emptyOn = !emptyOn);
              Sim.emptyMode = emptyOn;
              _controller.refresh();
            }),
            ActionButton(reachEndOn ? 'reachEndBuilder: on' : 'reachEndBuilder: off',
                tone: reachEndOn ? ActTone.accent : ActTone.normal,
                onTap: () => setState(() => reachEndOn = !reachEndOn)),
            ActionButton('Clear', onTap: _controller.clear),
          ]),
          ActionRow(label: 'Add', children: [
            ActionButton('+ Prepend', onTap: () => _controller.addItem(_synthetic(), prepend: true)),
            ActionButton('+ Append', onTap: () => _controller.addItem(_synthetic())),
            ActionButton('insertItem @2', onTap: () => _controller.insertItem(2, _synthetic())),
            ActionButton('insertItems ×3 @0', onTap: () => _controller.insertItems(0, [_synthetic(), _synthetic(), _synthetic()])),
          ]),
          ActionRow(label: 'Update', children: [
            ActionButton('updateItem first', onTap: _updateFirst),
            ActionButton('updateWhere', onTap: _updateWhere),
            ActionButton('updateFirstWhere', onTap: _updateFirstWhere),
            ActionButton('updateAll', onTap: _updateAll),
          ]),
          ActionRow(label: 'Replace', children: [
            ActionButton('replaceAt @1', onTap: () {
              if (_controller.count > 1) _controller.replaceAt(1, _synthetic(title: 'Replaced via replaceAt(1)'));
            }),
            ActionButton('replaceFirstWhere', onTap: _replaceFirstWhere),
            ActionButton('replaceWhere', onTap: _replaceWhere),
            ActionButton('replaceAll (shuffle)', onTap: () {
              final next = List<Object>.of(_controller.items)..shuffle();
              _controller.replaceAll(next);
            }),
          ]),
          ActionRow(label: 'Remove', children: [
            ActionButton('– Remove first', tone: ActTone.danger, onTap: () {
              final f = _first;
              if (f != null) _controller.removeItem(_keyOf(f));
            }),
            ActionButton('deleteWhere', tone: ActTone.danger, onTap: _deleteWhere),
            ActionButton('deleteFirstWhere', tone: ActTone.danger, onTap: _deleteFirstWhere),
          ]),
          ActionRow(label: 'Reorder', children: [
            ActionButton('moveItem 0→5', onTap: () => _controller.moveItem(0, 5)),
            ActionButton('moveToFirst 4', onTap: () => _controller.moveToFirst(4)),
            ActionButton('moveToLast 0', onTap: () => _controller.moveToLast(0)),
            ActionButton('swapItems 0↔3', onTap: () => _controller.swapItems(0, 3)),
          ]),
          ActionRow(label: 'Batch', children: [
            ActionButton('insertItemsBatch', onTap: () =>
                _controller.insertItemsBatch([BatchInsert<Object>(0, _synthetic()), BatchInsert<Object>(5, _synthetic())])),
            ActionButton('replaceItemsBatch', onTap: _replaceBatch),
            ActionButton('deleteItemsBatch', tone: ActTone.danger, onTap: _deleteBatch),
            ActionButton('moveItemsBatch', onTap: () => _controller.moveItemsBatch(const [BatchMove(0, 5), BatchMove(2, 0)])),
            ActionButton('transaction', tone: ActTone.accent, onTap: _txDemo),
          ]),
          ActionRow(label: 'Sort', children: [
            ActionButton('Flip order + resort', icon: Icons.swap_vert, onTap: _flipSort),
          ]),
          ActionRow(label: 'Select', children: [
            ActionButton('selectAll', onTap: _controller.selectAll),
            ActionButton('toggle first 3', onTap: () {
              for (final o in _controller.items.take(3)) {
                _controller.toggleSelected(_keyOf(o));
              }
              _log('selected ${_controller.selectedKeys.length}');
            }),
            ActionButton('clearSelection', onTap: _controller.clearSelection),
          ]),
          ActionRow(label: 'Scroll', children: [
            ActionButton('jumbToFirst', onTap: () => _controller.jumbToFirst()),
            ActionButton('jumbToLast', onTap: () => _controller.jumbToLast()),
            ActionButton('jumbTo 20', onTap: () => _controller.jumbTo(20)),
            ActionButton('jumbWhere last', onTap: _jumbWhereLast),
            ActionButton('animateToFirst', onTap: () => _controller.animateToFirst()),
            ActionButton('animateToLast', onTap: () => _controller.animateToLast()),
            ActionButton('animateTo 12', tone: ActTone.accent, onTap: () => _controller.animateTo(12, options: const ScrollOptions(alignment: 0.5))),
            ActionButton('moveTo 30', onTap: () => _controller.moveTo(30)),
            ActionButton('ensureVisibleAt 8', onTap: () => _controller.ensureVisibleAt(8, options: const ScrollOptions(alignment: 0.5))),
            ActionButton('ensureVisibleWhere', onTap: _ensureVisibleWhere),
          ]),
          ActionRow(label: 'Focus', children: [
            ActionButton('focusFirst', onTap: () => _controller.focusFirst()),
            ActionButton('focusLast', onTap: () => _controller.focusLast()),
            ActionButton('focusAt 3', onTap: () => _controller.focusAt(3)),
            ActionButton('focusWhere', onTap: _focusWhere),
            ActionButton('‹ focusPrev', onTap: () => _controller.focusPrevious()),
            ActionButton('focusNext ›', tone: ActTone.accent, onTap: () => _controller.focusNext()),
            ActionButton('clearFocus', onTap: _controller.clearFocus),
          ]),
        ],
      );

  void _updateFirst() {
    final f = _first;
    if (f is Post) {
      _controller.updateItem(f.id, (o) {
        final p = o as Post;
        return p.copyWith(liked: !p.liked, likes: p.liked ? p.likes - 1 : p.likes + 1);
      });
    } else if (f is Member) {
      _controller.updateItem(f.id, (o) {
        final m = o as Member;
        return m.copyWith(online: !m.online);
      });
    }
  }

  void _updateFirstWhere() {
    if (dataset == 'members') {
      _controller.updateFirstWhere(
        (o) => o is Member,
        (o) {
          final m = o as Member;
          return m.copyWith(online: !m.online);
        },
      );
    } else {
      _controller.updateFirstWhere(
        (o) => o is Post,
        (o) {
          final p = o as Post;
          return p.copyWith(liked: !p.liked, likes: p.liked ? p.likes - 1 : p.likes + 1);
        },
      );
    }
  }

  void _updateAll() {
    if (dataset == 'members') {
      _controller.updateAll((o) => (o as Member).copyWith(online: true));
    } else {
      _controller.updateAll((o) {
        final p = o as Post;
        return p.copyWith(liked: true);
      });
    }
  }

  void _updateWhere() {
    if (dataset == 'members') {
      _controller.updateWhere(
        (o) => o is Member && o.role == 'Member',
        (o) {
          final m = o as Member;
          return m.copyWith(role: 'Moderator');
        },
      );
    } else {
      _controller.updateWhere(
        (o) => o is Post && o.club == 'Madina IT',
        (o) {
          final p = o as Post;
          return p.copyWith(liked: true, likes: p.likes + 1);
        },
      );
    }
  }

  void _replaceFirstWhere() {
    final f = _first;
    if (f == null) return;
    _controller.replaceFirstWhere(
      (o) => _keyOf(o) == _keyOf(f),
      _synthetic(title: 'Replaced via replaceFirstWhere'),
    );
  }

  void _replaceWhere() {
    if (dataset == 'members') {
      _controller.replaceWhere(
        (o) => o is Member && o.role == 'Guest',
        (o) => (o as Member).copyWith(role: 'Member'),
      );
    } else {
      _controller.replaceWhere(
        (o) => o is Post && o.kind == 'article',
        (o) => (o as Post).copyWith(title: 'Re-published article'),
      );
    }
  }

  void _deleteWhere() {
    if (dataset == 'members') {
      _controller.deleteWhere((o) => o is Member && !o.online);
    } else {
      _controller.deleteWhere((o) => o is Post && o.kind == 'article');
    }
  }

  void _deleteFirstWhere() {
    if (dataset == 'members') {
      _controller.deleteFirstWhere((o) => o is Member && !o.online);
    } else {
      _controller.deleteFirstWhere((o) => o is Post && o.kind == 'article');
    }
  }

  void _replaceBatch() {
    final ks = _controller.items.take(2).map(_keyOf).toList();
    if (ks.length < 2) return;
    _controller.replaceItemsBatch([
      BatchReplace<Object>(ks[0], _synthetic(title: 'Batch-replaced #1')),
      BatchReplace<Object>(ks[1], _synthetic(title: 'Batch-replaced #2')),
    ]);
  }

  void _deleteBatch() {
    final ks = _controller.items.take(3).map(_keyOf).toList();
    if (ks.isNotEmpty) _controller.deleteItemsBatch(ks);
  }

  void _txDemo() {
    _controller.transaction(() {
      _controller.addItem(_synthetic(title: 'Added in a transaction'), prepend: true);
      final fifth = _at(4);
      if (fifth != null) _controller.removeItem(_keyOf(fifth));
      _controller.moveItem(0, 2);
    });
    _log('transaction → one notify');
  }

  void _flipSort() {
    final s = _sort;
    if (s == null) {
      _log('enable Sort first');
      return;
    }
    s.order = s.order == SortOrder.ascending ? SortOrder.descending : SortOrder.ascending;
    _controller.resort();
    _log('resort → ${s.order.name}');
  }

  void _jumbWhereLast() {
    if (_controller.items.isEmpty) return;
    final last = _controller.items.last;
    _controller.jumbWhere((o) => _keyOf(o) == _keyOf(last));
  }

  void _ensureVisibleWhere() {
    final target = _at(10);
    if (target == null) return;
    _controller.ensureVisibleWhere((o) => _keyOf(o) == _keyOf(target));
  }

  void _focusWhere() {
    final target = _at(5);
    if (target == null) return;
    _controller.focusWhere((o) => _keyOf(o) == _keyOf(target));
  }

  // --- stage: phone + browser + code reference --------------------------------
  Widget _stage() {
    final feed = dataset != 'members';
    final phoneTitle = feed ? 'News & articles' : 'Members';
    final phoneSub = '${mode == 'cursor' ? 'cursor-based' : 'page-based'} · $kind';
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        PhoneFrame(
          title: phoneTitle,
          subtitle: phoneSub,
          child: _view(
            tag: 'phone',
            itemBuilder: _phoneItem,
            padding: const EdgeInsets.all(14),
            gap: 12,
            crossAxisCount: 2,
            childAspectRatio: feed ? 0.74 : 0.85,
            pullToRefresh: true,
          ),
        ),
        const SizedBox(width: 22),
        Expanded(
          child: Column(
            children: [
              BrowserWindow(
                url: 'app.clubapp.com / ${feed ? 'feed' : 'members'}',
                height: 560,
                child: _view(
                  tag: 'browser',
                  itemBuilder: _browserItem,
                  padding: feed
                      ? const EdgeInsets.symmetric(horizontal: 20)
                      : const EdgeInsets.all(16),
                  gap: feed ? 0 : 12,
                  crossAxisCount: 3,
                  childAspectRatio: feed ? 0.9 : 1.0,
                  pullToRefresh: false,
                ),
              ),
              const SizedBox(height: 18),
              const CodeReferencePanel(height: 250),
            ],
          ),
        ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Footer / empty / error builders (ClubApp-styled)
// ---------------------------------------------------------------------------
class _LoadMore extends StatelessWidget {
  const _LoadMore();
  @override
  Widget build(BuildContext context) => const Padding(
        padding: EdgeInsets.all(18),
        child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
          SizedBox(width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2.4, color: CA.violet)),
          SizedBox(width: 10),
          Text('Loading more…', style: TextStyle(fontSize: 12, color: CA.ink500)),
        ]),
      );
}

class _NoMore extends StatelessWidget {
  const _NoMore();
  @override
  Widget build(BuildContext context) => Padding(
        padding: const EdgeInsets.all(18),
        child: Row(children: const [
          Expanded(child: Divider(color: CA.ink100)),
          Padding(
            padding: EdgeInsets.symmetric(horizontal: 10),
            child: Row(mainAxisSize: MainAxisSize.min, children: [
              Icon(Icons.check_rounded, size: 14, color: CA.success),
              SizedBox(width: 6),
              Text('You are all caught up', style: TextStyle(fontSize: 11, color: CA.ink400)),
            ]),
          ),
          Expanded(child: Divider(color: CA.ink100)),
        ]),
      );
}

/// Optional reach-end footer (shown when `hasReachedEnd` is true and the
/// `reachEndBuilder` slot is wired). Takes precedence over [_NoMore].
class _ReachEnd extends StatelessWidget {
  const _ReachEnd();
  @override
  Widget build(BuildContext context) => Padding(
        padding: const EdgeInsets.fromLTRB(18, 20, 18, 12),
        child: Center(
          child: Container(
            padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
            decoration: BoxDecoration(
              color: CA.violetSoft,
              borderRadius: BorderRadius.circular(999),
            ),
            child: Row(mainAxisSize: MainAxisSize.min, children: const [
              Icon(Icons.flag_rounded, size: 14, color: CA.violet),
              SizedBox(width: 7),
              Text('No more items',
                  style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: CA.violet)),
            ]),
          ),
        ),
      );
}

class _Empty extends StatelessWidget {
  const _Empty({required this.onRefresh, required this.dataset});
  final Future<void> Function() onRefresh;
  final String dataset;
  @override
  Widget build(BuildContext context) {
    final label = dataset == 'members' ? 'members' : 'posts';
    return Center(
      child: Column(mainAxisSize: MainAxisSize.min, children: [
        Container(
          width: 92,
          height: 92,
          decoration: BoxDecoration(color: CA.violet.withOpacity(0.08), borderRadius: BorderRadius.circular(26)),
          alignment: Alignment.center,
          child: Container(
            width: 54,
            height: 54,
            decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(18), boxShadow: CA.cardShadow),
            child: const Icon(Icons.inbox_outlined, color: CA.violet),
          ),
        ),
        const SizedBox(height: 16),
        Text('No $label found yet', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: CA.ink900)),
        const SizedBox(height: 6),
        SizedBox(
          width: 240,
          child: Text('Looks like you do not have any $label yet. You can refresh anytime.',
              textAlign: TextAlign.center, style: const TextStyle(fontSize: 13, color: CA.ink500)),
        ),
        const SizedBox(height: 16),
        FilledButton(style: FilledButton.styleFrom(backgroundColor: CA.violet), onPressed: onRefresh, child: const Text('Refresh')),
      ]),
    );
  }
}

class _Error extends StatelessWidget {
  const _Error({required this.error, required this.onRetry});
  final Object error;
  final Future<void> Function() onRetry;
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(mainAxisSize: MainAxisSize.min, children: [
        Container(
          width: 72,
          height: 72,
          decoration: BoxDecoration(color: CA.danger.withOpacity(0.1), borderRadius: BorderRadius.circular(22)),
          child: const Icon(Icons.wifi_off_rounded, color: CA.danger, size: 28),
        ),
        const SizedBox(height: 16),
        const Text('Something went wrong', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700, color: CA.ink900)),
        const SizedBox(height: 6),
        SizedBox(
          width: 260,
          child: Text('$error'.replaceAll('Exception: ', ''), textAlign: TextAlign.center, style: const TextStyle(fontSize: 13, color: CA.ink500)),
        ),
        const SizedBox(height: 16),
        FilledButton(style: FilledButton.styleFrom(backgroundColor: CA.violet), onPressed: onRetry, child: const Text('Retry')),
      ]),
    );
  }
}
1
likes
0
points
46
downloads

Documentation

Documentation

Publisher

unverified uploader

Weekly Downloads

A reusable MVC-style infinite-scroll pagination toolkit for Flutter with multiple providers (Future/Stream/sync), page or cursor-based pagination, reach-end state, item operations, batch transactions, optional sorting, key-based selection, bloc bindings, and multi-section Sliver support for mixed list and grid feeds.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

bloc, flutter

More

Packages that depend on scrolled_pagination