flex_infinity_view

Flexible infinite scrolling for Flutter lists, grids, and simple staggered layouts with loading, empty, and error states built in.

Features

  • Standalone and sliver-based infinite scrolling widgets
  • Support for ListView, GridView, and a two-column staggered layout
  • Vertical and horizontal infinite scrolling for list layouts
  • Automatic first-page loading and next-page loading on scroll
  • Explicit hasMore support so pagination can stop cleanly
  • Default initial loading with CircularProgressIndicator
  • Custom initial loading with loadingWidget
  • Custom widgets for loading, empty, error, and load-more states
  • External ScrollController support

Getting started

Add the package to your pubspec.yaml:

dependencies:
  flex_infinity_view: ^0.2.0

Usage

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

class ProductList extends StatefulWidget {
  const ProductList({super.key});

  @override
  State<ProductList> createState() => _ProductListState();
}

class _ProductListState extends State<ProductList> {
  final List<String> _items = [];

  bool _isLoading = false;
  bool _hasError = false;
  bool _hasMore = true;

  Future<void> _loadMore(int page, int limit) async {
    if (_isLoading) return;

    setState(() {
      _isLoading = true;
      _hasError = false;
    });

    try {
      await Future<void>.delayed(const Duration(milliseconds: 600));

      final nextItems = List.generate(
        limit,
        (index) => 'Item ${(page - 1) * limit + index + 1}',
      );

      setState(() {
        _items.addAll(nextItems);
        _hasMore = page < 4;
      });
    } catch (_) {
      setState(() => _hasError = true);
    } finally {
      setState(() => _isLoading = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return FlexInfinityView<String>(
      items: _items,
      limit: 8,
      isLoading: _isLoading,
      hasError: _hasError,
      hasMore: _hasMore,
      loadingWidget: const Center(child: CircularProgressIndicator()),
      onLoadMore: _loadMore,
      itemBuilder: (context, item, index) {
        return ListTile(title: Text(item));
      },
    );
  }
}

Layouts

Use layoutType to switch between supported presentations:

FlexInfinityView<MyItem>(
  layoutType: InfiniteLayoutType.grid,
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    childAspectRatio: 0.78,
  ),
  items: items,
  limit: 12,
  isLoading: isLoading,
  hasMore: hasMore,
  onLoadMore: fetchItems,
  itemBuilder: (context, item, index) => ProductCard(item: item),
)

Available values:

  • InfiniteLayoutType.list
  • InfiniteLayoutType.grid
  • InfiniteLayoutType.staggered

Horizontal lists

Set scrollDirection to Axis.horizontal when using the list layout. Give the view a bounded height and each item a suitable width.

SizedBox(
  height: 240,
  child: FlexInfinityView<Product>(
    scrollDirection: Axis.horizontal,
    items: products,
    limit: 12,
    isLoading: isLoading,
    hasMore: hasMore,
    onLoadMore: fetchProducts,
    itemBuilder: (context, product, index) => SizedBox(
      width: 280,
      child: ProductCard(item: product),
    ),
  ),
)

Horizontal direction is supported only with InfiniteLayoutType.list. Vertical remains the default, so existing integrations keep their behavior.

Sliver usage

Use FlexInfinitySliverView when you need to place the package inside an existing CustomScrollView.

CustomScrollView(
  slivers: [
    const SliverAppBar(
      pinned: true,
      title: Text('Products'),
    ),
    FlexInfinitySliverView<Product>(
      items: items,
      limit: 12,
      isLoading: isLoading,
      hasError: hasError,
      hasMore: hasMore,
      layoutType: InfiniteLayoutType.grid,
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        childAspectRatio: 0.78,
      ),
      onLoadMore: fetchProducts,
      itemBuilder: (context, item, index) => ProductCard(item: item),
    ),
  ],
)

A sliver follows its parent scroll axis. For a horizontal sliver list, set scrollDirection: Axis.horizontal on CustomScrollView and use InfiniteLayoutType.list on FlexInfinitySliverView.

SizedBox(
  height: 240,
  child: CustomScrollView(
    scrollDirection: Axis.horizontal,
    slivers: [
      FlexInfinitySliverView<Product>(
        layoutType: InfiniteLayoutType.list,
        items: products,
        limit: 12,
        isLoading: isLoading,
        hasMore: hasMore,
        onLoadMore: fetchProducts,
        itemBuilder: (context, product, index) => SizedBox(
          width: 400,
          child: ProductCard(item: product),
        ),
      ),
    ],
  ),
)

Do not set a separate direction on FlexInfinitySliverView: Flutter slivers inherit their main axis from the parent scroll view. As with standalone horizontal lists, give the viewport a bounded height and each item a finite width.

State handling

The widget supports:

  • Initial loading with loadingWidget
  • Empty state with emptyWidget
  • Error state with errorWidget
  • Load-more state with loadMoreWidget
  • Pagination stop with hasMore

Example

The example/ app demonstrates:

  • FlexInfinityView as a standalone scrollable
  • FlexInfinitySliverView inside CustomScrollView
  • Vertical and horizontal standalone lists
  • Vertical grid and horizontal list sliver composition
  • List, grid, and staggered layouts
  • Default loading and custom loading states
  • Empty responses
  • Initial request errors
  • Next-page request errors with retry

Run it with:

flutter run -d chrome example/lib/main.dart

Additional information

Before publishing, run:

flutter analyze
flutter test
flutter pub publish --dry-run

Package page guidance used for this setup:

Libraries

flex_infinity_view