infinite_scroll_pagination icon indicating copy to clipboard operation
infinite_scroll_pagination copied to clipboard

Multiple quick refreshes causes loosing of first page

Open Nazarii77 opened this issue 2 years ago • 12 comments

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

Nazarii77 avatar Feb 11 '22 17:02 Nazarii77

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.

clragon avatar Feb 12 '22 19:02 clragon

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 avatar Feb 14 '22 14:02 Nazarii77

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 avatar Feb 22 '22 04:02 FarhanSajid1

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 avatar Feb 22 '22 10:02 FarhanSajid1

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?

FarhanSajid1 avatar Feb 22 '22 10:02 FarhanSajid1

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

FlorianBasso avatar Feb 24 '22 15:02 FlorianBasso

I have also this same issue

Veeksi avatar Apr 22 '22 13:04 Veeksi

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;
    }
  }
}

InAnadea avatar Apr 25 '22 14:04 InAnadea

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

jagmohanJelly avatar May 16 '22 11:05 jagmohanJelly

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;
    }
  }

geniuspegasus avatar Jun 03 '22 07:06 geniuspegasus

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 :)

HemangSidapara avatar Jan 20 '23 12:01 HemangSidapara

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

CoolDude53 avatar May 05 '23 19:05 CoolDude53