EdsonBueno / infinite_scroll_pagination

Flutter package to help you lazily load and display pages of items as the user scrolls down your screen.
https://pub.dev/packages/infinite_scroll_pagination
MIT License
605 stars 201 forks source link

Next page loading automatically with error #286

Closed lucasshiva closed 10 months ago

lucasshiva commented 10 months ago

When an error occurs during loading subsequent pages, the package doesn't wait for the user to tap to retry—it skips the current page and loads the next page automatically.

For example: If trying to fetch page 2 returns an error, the error message is shown, but since the scroll is still at the bottom, it then tries to load page 3, completely ignoring the error of page 2.

What should happen instead: Fetching page 2 returns an error. Error widget is shown to the user. No more pages are fetched until the user taps on retry, which will show the subsequent loading again while trying to fetch the same page that returned an error, in this case, page 2.

This is my widget:

import 'dart:developer';

import 'package:flutter/material.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:pagination/item_cubit.dart';

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

  @override
  State<ItemGrid> createState() => _ItemGridState();
}

class _ItemGridState extends State<ItemGrid> {
  final PagingController pagingController = PagingController(firstPageKey: 1);
  final ItemCubit itemCubit = ItemCubit();

  @override
  void initState() {
    super.initState();
    pagingController.addPageRequestListener((pageKey) async {
      log("Fetching page: $pageKey");
      final itemState = await itemCubit.fetchItems(pageKey);
      final isLastPage = itemState.currentPage == itemState.lastPage;
      if (isLastPage) {
        pagingController.appendLastPage(itemState.items);
      } else {
        pagingController.appendPage(itemState.items, pageKey + 1);
      }
      pagingController.error = itemState.error;
    });
  }

  @override
  void dispose() {
    pagingController.dispose();
    itemCubit.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return RefreshIndicator(
      onRefresh: () async {
        pagingController.refresh();
      },
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 8),
        child: PagedGridView(
          showNewPageErrorIndicatorAsGridChild: false,
          showNewPageProgressIndicatorAsGridChild: false,
          showNoMoreItemsIndicatorAsGridChild: false,
          pagingController: pagingController,
          builderDelegate: PagedChildBuilderDelegate(
            itemBuilder: (context, item, index) {
              return Container(
                height: 200,
                width: 200,
                decoration: BoxDecoration(
                  border: Border.all(
                    color: theme.colorScheme.primary,
                    width: 1,
                  ),
                ),
                child: Center(
                  child: Text(
                    item.toString(),
                    style: theme.textTheme.headlineLarge,
                  ),
                ),
              );
            },
          ),
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 2,
            mainAxisSpacing: 8,
            crossAxisSpacing: 8,
          ),
        ),
      ),
    );
  }
}

And the cubit:

import 'dart:developer' show log;
import 'dart:math' show Random;

import 'package:flutter_bloc/flutter_bloc.dart';

const int itemsPerPage = 24;
const int lastItemsPage = 3;

final class ItemState {
  ItemState({
    this.items = const <int>[],
    this.currentPage = 1,
    this.lastPage = 1,
    this.error,
  });

  final List<int> items;
  final int currentPage;
  final int lastPage;
  final dynamic error;
}

final class ItemCubit extends Cubit<ItemState> {
  ItemCubit() : super(ItemState());

  Future<ItemState> fetchItems(int page) async {
    // Simulate loading.
    await Future.delayed(const Duration(milliseconds: 700));

    try {
      final newItems = getItems(page);
      return ItemState(
        items: newItems,
        currentPage: page,
        lastPage: lastItemsPage,
        error: null,
      );
    } catch (e) {
      log("Got an error!");
      return ItemState(
        error: e,
        lastPage: lastItemsPage,
        currentPage: page,
      );
    }
  }

  List<int> getItems(int page) {
    // Throw an error 50% of times.
    if (Random().nextInt(2) == 1) {
      throw Exception("Error loading items");
    }

    return List.generate(
      itemsPerPage,
      (index) {
        // Page 1: 1-24;
        // Page 2: 25-48;
        // ...
        final item = itemsPerPage * (page - 1) + (index + 1);
        return item;
      },
    );
  }
}

For the record, the page using ItemGrid is a simple StatelessWidget with a Scaffold.

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Items"),
      ),
      body: const ItemGrid(),
    );
  }
}

This was tested on Flutter 3.13.0 stable with Dart 3.1.0. Package version is the latest 4.0.0.

lucasshiva commented 10 months ago

Figured out the error. It was totally my fault. I needed to put the try/catch inside the addPageRequestListener instead of inside the cubit itself. Like so:

pagingController.addPageRequestListener((pageKey) async {
      log("Fetching page: $pageKey");

      try {
        final itemState = await itemCubit.fetchItems(pageKey);
        final isLastPage = itemState.currentPage == itemState.lastPage;
        if (isLastPage) {
          pagingController.appendLastPage(itemState.items);
        } else {
          pagingController.appendPage(itemState.items, pageKey + 1);
        }
      } catch (e) {
        pagingController.error = e;
      }
    });

And also remove it from the cubit, of course:

Future<ItemState> fetchItems(int page) async {
    // Simulate loading.
    await Future.delayed(const Duration(milliseconds: 700));

    final newItems = getItems(page);
    return ItemState(
      items: newItems,
      currentPage: page,
      lastPage: lastItemsPage,
      error: null,
    );
  }