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
632 stars 214 forks source link

Multiple quick refreshes causes loosing of first page #172

Closed Nazarii77 closed 7 months ago

Nazarii77 commented 2 years ago

If you refresh multiple times quickly - some of the refreshes may fail and skip page

clragon commented 2 years ago

The package does not prevent running multiple refreshes at once, which can lead to the data being messed up.

You can try to prevent multiple refreshes from happening by having a boolean or mutex control your refresh function.

Nazarii77 commented 2 years ago

Thanks, @clragon, flags helped, but not fully. If the page size is small like 3, the page 0 and 1 are loading almost simultaniously on big screens. Original counter messes up and sometimes I see 1 page before page 0. Also there is need to block refresh if we currently loading some big page, like 10 items of huge json`s

//in class declaration bool _isLoadingStarted = false;

//in init @override void initState() { super.initState(); _pagingController.addPageRequestListener((pageKey) async { if ((_pagingController.value.status == PagingStatus.loadingFirstPage || _pagingController.value.status == PagingStatus.ongoing) && _isLoadingStarted) return; _isLoadingStarted = true; await _fetchPage(Provider.of<YourProvider>(context, listen: false), pageKey, widget.YourItemId); }); }

//on refresh

onRefresh: () async { if ((_pagingController.value.status == PagingStatus.loadingFirstPage || _pagingController.value.status == PagingStatus.ongoing) && _isLoadingStarted) return; extensions.clear(); _pagingController.refresh(); },

FarhanSajid1 commented 2 years ago

New to flutter, but this seems like a critical issue? I would've hoped that there would be some locking mechanism that would be in place to prevent something like this

FarhanSajid1 commented 2 years ago

The package does not prevent running multiple refreshes at once, which can lead to the data being messed up.

You can try to prevent multiple refreshes from happening by having a boolean or mutex control your refresh function.

Is there an example on the proper way to do this?

FarhanSajid1 commented 2 years ago

Thanks, @clragon, flags helped, but not fully. If the page size is small like 3, the page 0 and 1 are loading almost simultaniously on big screens. Original counter messes up and sometimes I see 1 page before page 0. Also there is need to block refresh if we currently loading some big page, like 10 items of huge json`s

//in class declaration bool _isLoadingStarted = false;

//in init @OverRide void initState() { super.initState(); _pagingController.addPageRequestListener((pageKey) async { if ((_pagingController.value.status == PagingStatus.loadingFirstPage || _pagingController.value.status == PagingStatus.ongoing) && _isLoadingStarted) return; _isLoadingStarted = true; await _fetchPage(Provider.of<YourProvider>(context, listen: false), pageKey, widget.YourItemId); }); }

//on refresh

onRefresh: () async { if ((_pagingController.value.status == PagingStatus.loadingFirstPage || _pagingController.value.status == PagingStatus.ongoing) && _isLoadingStarted) return; extensions.clear(); _pagingController.refresh(); },

@Nazarii77 how would one implement this inside of _fetchpage?

FlorianBasso commented 2 years ago

I think I have the same issue when I refresh the page controller after a search.

Veeksi commented 2 years ago

I have also this same issue

InAnadea commented 2 years ago

I have the same issue when trying to implement searching.

There is my code for paged list view and search. Search triggered inside didUpdateWidget and calling refresh().

class PostsFeedListView extends StatefulWidget {
  const PostsFeedListView({
    Key? key,
    required this.classId,
    required this.basePostsRequest,
    this.pageSize = 3,
    this.controller,
  })  : assert(pageSize > 0),
        super(key: key);

  final String classId;
  final PostsRequest basePostsRequest;
  final int pageSize;
  final ScrollController? controller;

  @override
  State<PostsFeedListView> createState() => _PostsFeedListViewState();
}

class _PostsFeedListViewState extends State<PostsFeedListView>
    with LocalizationsMixin {
  final PagingController<int, Post> _pagingController =
      PagingController(firstPageKey: 0);

  final PagingController<int, Post> _pinnedListPagingController =
      PagingController(firstPageKey: 0);

  late PostsRequest _postsRequest;

  late PostsRequest _pinnedPostsRequest;

  bool _isShowPinned = true;

  Completer? _refreshCompleter;

  bool _isLoading = true;
  bool _isPinnedLoading = true;

  @override
  void initState() {
    super.initState();
    _initPostRequests();
    _initControllers();
  }

  @override
  void didUpdateWidget(covariant PostsFeedListView oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.basePostsRequest != oldWidget.basePostsRequest) {
      if (_refreshCompleter != null) {
        _refreshCompleter?.future.then((_) async {
          _isShowPinned = true;
          _updateControllers();
        });
      } else {
        _updateControllers();
      }
    }
  }

  void _updateControllers() {
    _disposeControllers();
    _initPostRequests();
    _initControllers();
    _onRefresh();
  }

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

  void _initControllers() {
    _pinnedListPagingController.addPageRequestListener(_fetchPageOfPinnedPosts);
    _pagingController.addPageRequestListener(_fetchPage);
  }

  void _disposeControllers() {
    _pinnedListPagingController
        .removePageRequestListener(_fetchPageOfPinnedPosts);
    _pagingController.removePageRequestListener(_fetchPage);
  }

  void _initPostRequests() {
    _postsRequest = widget.basePostsRequest.copyWith(pinnedOnly: false);
    _pinnedPostsRequest = widget.basePostsRequest.copyWith(pinnedOnly: true);
  }

  void _fetchPage(int pageKey) {
    BlocProvider.of<FeedBloc>(context).add(
      GetPosts(
        widget.classId,
        pageParameters: PageParameters(
          number: pageKey,
          size: widget.pageSize,
        ),
        requestParameters: _postsRequest,
      ),
    );
  }

  void _fetchPageOfPinnedPosts(int pageKey) {
    BlocProvider.of<FeedBloc>(context).add(
      GetPosts(
        widget.classId,
        pageParameters: PageParameters(
          number: pageKey,
          size: widget.pageSize,
        ),
        requestParameters: _pinnedPostsRequest,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return MultiBlocListener(
      listeners: [
        BlocListener<FeedBloc, FeedState>(
          listener: _feedListener,
        ),
        BlocListener<PostsBloc, PostsState>(
          listener: _postsUpdatesListener,
        ),
      ],
      child: RefreshIndicator(
        onRefresh: _onRefresh,
        child: CustomScrollView(
          slivers: [
            if (_isShowPinned)
              PagedSliverList(
                pagingController: _pinnedListPagingController,
                builderDelegate: _getPageDelegate(),
              ),
            PagedSliverList<int, Post>(
              pagingController: _pagingController,
              builderDelegate: _getPageDelegate(),
            ),
          ],
        ),
      ),
    );
  }

  void _postsUpdatesListener(context, state) async {
    if (state is PostPublished && state.classId == widget.classId ||
        state is PostScheduled && state.classId == widget.classId) {
      widget.controller?.jumpTo(0);
      await _onRefresh();
    }
    if (state is PostUpdated && state.classId == widget.classId) {
      BlocProvider.of<PostsBloc>(context).add(GetPost(
        state.classId,
        state.postId,
      ));
    }
    if (state is PollUpdated && state.classId == widget.classId) {
      BlocProvider.of<PostsBloc>(context).add(GetPost(
        state.classId,
        state.postId,
      ));
    }
  }

  Future<void> _onRefresh() {
    _isLoading = true;
    _isPinnedLoading = true;
    _refreshCompleter?.complete();
    _pinnedListPagingController.refresh();
    _pagingController.refresh();
    _refreshCompleter = Completer();
    return _refreshCompleter!.future;
  }

  PagedChildBuilderDelegate<Post> _getPageDelegate() {
    return PagedChildBuilderDelegate<Post>(
      itemBuilder: (context, item, index) => PostListTile(
        key: ValueKey(widget.classId + item.id),
        post: item,
        classId: widget.classId,
      ),
      animateTransitions: true,
      noItemsFoundIndicatorBuilder: (context) => Center(
        child: Padding(
          padding: const EdgeInsets.all(20.0),
          child: Text(
            localizations.thereIsNothingHere,
            style: Theme.of(context).textTheme.headline5,
          ),
        ),
      ),
    );
  }

  void _feedListener(context, state) {
    if (state is PostsReceived && state.requestParameters == _postsRequest) {
      _isLoading = false;
      _updateCompleter();
      final isLastPage = state.page.content.length < widget.pageSize;
      if (isLastPage) {
        _pagingController.appendLastPage(state.page.content);
      } else {
        _pagingController.appendPage(
          state.page.content,
          state.page.number + 1,
        );
      }
    } else if (state is PostsReceived &&
        state.requestParameters == _pinnedPostsRequest) {
      _isPinnedLoading = false;
      _updateCompleter();
      final isLastPage = state.page.content.length < widget.pageSize;
      if (isLastPage) {
        _pinnedListPagingController.appendLastPage(state.page.content);
        if (_pinnedListPagingController.itemList?.isEmpty ?? true) {
          setState(() {
            _isShowPinned = false;
          });
        }
      } else {
        _pinnedListPagingController.appendPage(
          state.page.content,
          state.page.number + 1,
        );
      }
    }
  }

  void _updateCompleter() {
    if (!_isLoading && !_isPinnedLoading) {
      _refreshCompleter?.complete();
      _refreshCompleter = null;
    }
  }
}
jagmohanJelly commented 2 years ago

I have also faced this issue then I find out that we are facing this issue because whenever we call _pageController.refresh(); if there is an already ongoing call so it will first complete that call and after that, it calls the refresh function due to which we got the already ongoing request's items first and then the refreshed items. so to get rid of this I just added a small functionality is whenever it loads the first page then it also clears the existing item list

Added this code in the _fetchPage method and it works fine for me

 if (pageKey == 1) {
        if (_pagingController.itemList != null) {
          _pagingController.itemList!.clear();
        }
      }

after adding this to my _fetchPageMethod

  _fetchPage(int pageKey) async {
    try {
      final newItems = await Repository.getData(pageNumber: pageKey);
      if (newItems == null) {
        return;
      }
      if (pageKey == 1) {
        if (_pagingController.itemList != null) {
          _pagingController.itemList!.clear();
        }
      }
      final isLastPage = newItems.length < _pageSize;
      if (isLastPage) {
        _pagingController.appendLastPage(newItems);
      } else {
        final nextPageKey = pageKey + 1;
        _pagingController.appendPage(newItems, nextPageKey);
      }
    } catch (error) {
      _pagingController.error = error;
    }
  }

hope this helped

geniuspegasus commented 2 years ago

I have also faced this issue then I find out that we are facing this issue because whenever we call _pageController.refresh(); if there is an already ongoing call so it will first complete that call and after that, it calls the refresh function due to which we got the already ongoing request's items first and then the refreshed items. so to get rid of this I just added a small functionality is whenever it loads the first page then it also clears the existing item list

Added this code in the _fetchPage method and it works fine for me

 if (pageKey == 1) {
        if (_pagingController.itemList != null) {
          _pagingController.itemList!.clear();
        }
      }

after adding this to my _fetchPageMethod

  _fetchPage(int pageKey) async {
    try {
      final newItems = await Repository.getData(pageNumber: pageKey);
      if (newItems == null) {
        return;
      }
      if (pageKey == 1) {
        if (_pagingController.itemList != null) {
          _pagingController.itemList!.clear();
        }
      }
      final isLastPage = newItems.length < _pageSize;
      if (isLastPage) {
        _pagingController.appendLastPage(newItems);
      } else {
        final nextPageKey = pageKey + 1;
        _pagingController.appendPage(newItems, nextPageKey);
      }
    } catch (error) {
      _pagingController.error = error;
    }
  }

hope this helped

I tweaked your answer a bit and it now works perfectly for me.

fetchPage(int pageKey) async {
    try {
      final newItems = await Repository.getData(pageNumber: pageKey);
      if (newItems == null) {
        return;
      }
      if (pageKey == 1) {
        if (_pagingController.itemList != null) {
          _pagingController.itemList!.clear();
        }
      }
      final isLastPage = newItems.length < _pageSize;

//I only add an element if I am on the first page and the list is empty or if I am the page different from 1 and the list is not //empty
 if ((pageKey == 1 && _pagingController.itemList == null) ||
            (pageKey > 1 && _pagingController.itemList != null)) {
      if (isLastPage) {
        _pagingController.appendLastPage(newItems);
      } else {
        final nextPageKey = pageKey + 1;
        _pagingController.appendPage(newItems, nextPageKey);
      }
}
    } catch (error) {
      _pagingController.error = error;
    }
  }
HemangSidapara commented 1 year ago

same issue goes with me, when i pass quickly search value or quickly clear value then refresh() skip the last some calls. and list not show proper items, so i used https://pub.dev/packages/debounce_throttle, its delay the refresh() for some time and then call next refresh()

  Debouncer<String> debouncer = Debouncer<String>(const Duration(milliseconds: 200),initialValue: '');
  TextEditingController searchController = TextEditingController();

  void controllerListener(){
    searchController.addListener(() {
      debouncer.value = searchController.text;
    });
  }

in textfield

onChanged: (value) {
  controller.debouncer.values.listen((event) {
    controller.searchedPersonName = event;
    controller.pagingController.refresh();
  });
},

this is worked for me :)

CoolDude53 commented 1 year ago

Linking to better solution: https://github.com/EdsonBueno/infinite_scroll_pagination/issues/108#issuecomment-1004731033