Hexsis Infinity Scroll - BLoC Extension

Zero-boilerplate BLoC/Cubit integration with automatic scroll detection and state management.

Installation

dependencies:
  hexsis_infinity_scroll_bloc: ^0.2.2

Complete Example

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hexsis_infinity_scroll_bloc/hexsis_infinity_scroll_bloc.dart';

// 1. Define your cubit (just implement fetchPage!)
class ProductCubit extends InfiniteScrollCubit<Product> {
  @override
  Future<List<Product>> fetchPage(int page) async {
    return await api.getProducts(page: page, limit: 20);
  }
}

// 2. Use the widget (that's it!)
class ProductListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => ProductCubit(),
      child: Scaffold(
        appBar: AppBar(title: Text('Products')),
        body: InfiniteScrollBlocListView<Product, ProductCubit>(
          itemBuilder: (context, product, index) {
            return ProductCard(product);
          },
        ),
      ),
    );
  }
}

That's it! The widget automatically:

  • ✅ Detects scroll threshold and calls cubit.loadMore()
  • ✅ Shows loading indicator on initial load
  • ✅ Shows error widget with retry button
  • ✅ Shows empty state when no items
  • ✅ Shows loading more indicator at bottom
  • ✅ Handles pull-to-refresh
  • ✅ Uses smart threshold (500px from bottom for long lists)

GridView Support

InfiniteScrollBlocGridView<Product, ProductCubit>(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    crossAxisSpacing: 8,
    mainAxisSpacing: 8,
  ),
  itemBuilder: (context, product, index) => ProductCard(product),
)

Customization

Custom UI Components

InfiniteScrollBlocListView<Product, ProductCubit>(
  itemBuilder: (context, product, index) => ProductCard(product),
  
  // Custom builders
  separatorBuilder: (context, index) => Divider(height: 1),
  loadingBuilder: (context) => CustomLoadingWidget(),
  errorBuilder: (context, error, retry) => CustomErrorWidget(error, retry),
  emptyBuilder: (context) => CustomEmptyWidget(),
  loadingMoreBuilder: (context) => CustomLoadingMoreWidget(),
)

Configuration

InfiniteScrollBlocListView<Product, ProductCubit>(
  itemBuilder: (context, product, index) => ProductCard(product),
  config: InfiniteScrollConfig(
    threshold: 0.9,              // Load at 90% (small lists)
    thresholdPixels: 300,        // 300px from bottom (large lists)
    thresholdBreakpoint: 3000,   // Switch point
    debounceMilliseconds: 200,   // Faster response
    enablePullToRefresh: true,
  ),
)

How It Works

InfiniteScrollCubit

Base cubit that handles all pagination logic:

abstract class InfiniteScrollCubit<T> extends Cubit<InfiniteScrollState<T>> {
  // You implement this:
  Future<List<T>> fetchPage(int page);
  
  // Already implemented for you:
  Future<void> loadPage(int page, {bool isRefresh});
  Future<void> refresh();
  Future<void> loadMore();
}

State managed automatically:

  • items - Current list
  • currentPage - Page number
  • hasMore - More items available
  • isLoading, isLoadingMore, isRefreshing
  • error, stackTrace

Widget Integration

The widget:

  1. Creates ScrollController
  2. Listens to scroll events
  3. Calculates smart threshold
  4. Calls cubit.loadMore() at threshold
  5. Uses BlocBuilder to rebuild on state changes
  6. Handles all UI states automatically

You write ~5 lines, get full infinite scroll! 🎉

Advanced Usage

Manual Control

// Access cubit directly if needed
BlocBuilder<ProductCubit, InfiniteScrollState<Product>>(
  builder: (context, state) {
    return Column(
      children: [
        Text('Loaded ${state.items.length} items'),
        if (state.hasMore)
          ElevatedButton(
            onPressed: () => context.read<ProductCubit>().loadMore(),
            child: Text('Load More'),
          ),
      ],
    );
  },
)

Error Handling

class ProductCubit extends InfiniteScrollCubit<Product> {
  @override
  Future<List<Product>> fetchPage(int page) async {
    try {
      return await api.getProducts(page);
    } catch (e) {
      // Error automatically captured and shown in UI
      rethrow;
    }
  }
}

Comparison

Without this package:

// 60+ lines of boilerplate:
// - Manual state class
// - Manual loading states
// - Manual scroll detection
// - Manual threshold calculation
// - Manual error handling
// - Manual UI building

With this package:

// 5 lines total:
class ProductCubit extends InfiniteScrollCubit<Product> {
  Future<List<Product>> fetchPage(int page) async => api.getProducts(page);
}

InfiniteScrollBlocListView<Product, ProductCubit>(
  itemBuilder: (context, product, index) => ProductCard(product),
)

95% less code! 🚀

Reliability

The cubit ignores fetch completions after it has been closed, preventing late async work from emitting into disposed state. Behavioral tests cover loading, pagination, errors, retries, lifecycle safety, and list/grid widget integration.

License

MIT License - Copyright (c) 2025 Hexsis Enterprise LLC