Infinite Paging ScrollView

Encapsulate animated_infinite_scroll_pagination package with common (loading, footer, error) widgets and functionality.

Attributes:

Name
Type
Description
Default Value
Required
Nullable
controller
AnimatedInfinitePaginationController<T>
class extends [AnimatedInfinitePaginationController]-
itemBuilder
Widget Function(
  BuildContext context, 
  T item, 
  int index,
)
callback return [Widget] with item [T], to display item card in listView-
topWidgets
List<{bool isSliver, Widget child}>
pass [topWidgets] when you want to place a widget at the top of the first [itemBuilder] widget.-
onRefresh
Function()
callback called when user swipe to refresh list.-
retry
Function()
callback called when pagination request failed and user press on [errorWidget].-
refreshIndicator
bool
wrap [ScrollView] in [RefreshIndicator].
true
scrollDirection
Axis
The [Axis] along which the scroll view's offset increases.
Axis.vertical
gridDelegate
SliverGridDelegate
A delegate that controls the layout of the children within the [GridView].-
spawnIsolate
bool
Whether to spawn a new isolate on which to calculate the diff on.-
noItemsWidget
Widget
widget appears after fetch first page data and the result is empty.-
loadingWidget
Widget
widget appears when first page is in loading state.-

Implementing AnimatedInfinitePaginationController

Paging Controller works on fetch parts of data and hold this data, handle events like (refreshIndicator) event,We recommend implementing PagingController with Scoped SingletonHere is example to fetch ListResponse of Products with paging:
import 'package:injectable/injectable.dart';
import 'package:animated_infinite_scroll_pagination/animated_infinite_scroll_pagination.dart';

@LazySingleton(scope: 'productsPaging')
class ProductsPagingController with AnimatedInfinitePaginationController<Product> {
  final ProductsRepository _repository;
  ProductsPagingController(this._repository);

  /// decide whether two object represent the same Item
  @override
  bool areItemsTheSame(Product oldItem, Product newItem) {
    return oldItem.id == newItem.id;
  }

  /// fetch data from repository and emit new state
  @override
  Future<void> fetchData(int page) async {
    // emit loading
    emitState(const PaginationLoadingState());

    // fetch data from server
    try {
      await for (final response in _repository.findAll(page)) {
        final result = response.data;
        if (result?.data != null && response.isSuccess) {
          // emit fetched data
          //
          // when emit remote data after cached data with same page,
          // controller will replace the cached data with remote data
          emitState(PaginationSuccessState(result!.data!.toList(), cached: result.cached));

          // tell the controller the total of items,
          // this will stop loading more data when last data-chunk is loaded.
          setTotal(result.total!);
        } else {
          // emit error
          emitState(const PaginationErrorState());
        }
      }
    } catch (error) {
      // emit error
      emitState(const PaginationErrorState());
      if (kDebugMode) print(error);
    }
  }

  /// cancel all pending requests
  void dispose() {
    _repository.dispose();
  }
}

Implementing AnimatedInfiniteScrollView

  • products_list_screen.dart
  • products_list_viewmodel.dart
class ProductsListScreen extends StatefulWidget {
  const ProductsListScreen({super.key});

  @override
  State<ProductsListScreen> createState() => _ProductsListScreenState();
}

class _ProductsListScreenState extends State<ProductsListScreen> 
  with LifecycleOwner<ProductsListScreen, ProductsListViewModel>, ObserverMixin {

  @override
  DiScope get diScope => DiScope(
    name: 'productsList', // (viewModel) scope
    factory: () => GetIt.instance.initProductsListScope(),
    dependencies: [
      // be sure to make the 'initProductsScope' before 'initProductsPagingScope'
      DiScope(
        name: 'products', // (repository and data-source) scope
        factory: () => getIt.initProductsScope(),
      ),
      DiScope(
        name: 'productsPaging', // (paging controller) scope
        factory: () => getIt.initProductsPagingScope(),
      ),
    ],
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: AnimatedInfiniteScrollView<Product>(
        controller: GetIt.instance<ProductsPagingController>(), // injected pagingController
        options: AnimatedInfinitePaginationOptions(
          loadingWidget: const CircularProgressIndicator.adaptive(), // Displayed when loading first page
          errorWidget: const Center(child: Text('An error occurred')), // Displayed on error
          itemBuilder: (BuildContext context, Product product, int index) {
            // Build the UI for each product item
            return ListTile(
              title: Text(product.name!),
              subtitle: Text(product.description!),
            );
          },
        ),
      ),
    );
  }
}