quire-io / scroll-to-index

scroll to index with fixed/variable row height inside Flutter scrollable widget
MIT License
511 stars 104 forks source link

Failed (offsetToLastState?.offset ?? 0) >= 0 #37

Closed allComputableThings closed 3 years ago

allComputableThings commented 3 years ago

I'm getting the following error.

In my case:

The error happens occasionally.

After the error, isAutoScrolling does not return to true unless another scrollToIndex is requested.

Any ideas? I haven't been able to produce a reasonably sized reproduction example yet. Any ideas what the problem may be? Thanks in advance.

E/flutter (12779): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: 'package:scroll_to_index/scroll_to_index.dart': Failed assertion: line 339 pos 14: '(offsetToLastState?.offset ?? 0) >= 0': ERROR: %%%%%%%%%%%%%%: 15, 0, 1.0, RevealedOffset(offset: -408.8262626669658, rect: Rect.fromLTRB(NaN, NaN, NaN, NaN)), 0,1,4,6,8,10,12,14,17,19,21,24,26,28,30,31,32,33,34,35,36,37,38,39,40
E/flutter (12779): #0      _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:46:39)
E/flutter (12779): #1      _AssertionError._throwNew (dart:core-patch/errors_patch.dart:36:5)
E/flutter (12779): #2      AutoScrollControllerMixin._forecastMoveUnit (package:scroll_to_index/scroll_to_index.dart:339:14)
E/flutter (12779): #3      AutoScrollControllerMixin._scrollToIndex (package:scroll_to_index/scroll_to_index.dart:252:28)
E/flutter (12779): <asynchronous suspension>
E/flutter (12779): #4      AutoScrollControllerMixin.scrollToIndex.<anonymous closure> (package:scroll_to_index/scroll_to_index.dart:196:27)
E/flutter (12779): #5      co (package:scroll_to_index/util.dart:37:26)
E/flutter (12779): #6      AutoScrollControllerMixin.scrollToIndex (package:scroll_to_index/scroll_to_index.dart:196:12)
jerrywell commented 3 years ago

Hi, do you wrap the all children of ListView(or other type of SliverList) with AutoScrollTag?

this assertion is actually making sure we won't set the list scroll offset to minus value. if your list accept the minus offset as the working scroll offset, I'm afraid of that you probably can't use this package.

allComputableThings commented 3 years ago

Thanks. Yes. I wrapped the elements and I’m never asking for a negative offset, or index. After a little digging, I think it’s happening after the ListView is rebuilt (because additional data arrives for each list element from a web request after the first time the list is shown). The number of items and their widget keys are unchanged. Is there a way to reset the discovered state in the AutoScrollController? It lives in a widget state in my app and the listview is recreated in its build method, but not the list controller.

On Wed, Nov 25, 2020 at 8:27 PM Jerry Chen notifications@github.com wrote:

Hi, do you wrap the all children of ListView(or other type of SliverList) with AutoScrollTag?

this assertion is actually making sure we won't set the list scroll offset to minus value. if your list accept the minus offset as the working scroll offset, I'm afraid of that you probably can't use this package.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/quire-io/scroll-to-index/issues/37#issuecomment-734066624, or unsubscribe https://github.com/notifications/unsubscribe-auth/AB3QJLCDLARQPRZO4UH3B33SRXKLJANCNFSM4UCW3I3Q .

jerrywell commented 3 years ago

the straightforward solution seems that you may need a flag to indicate the AutoScrollController is searching. e.g.

Future scrolling;

/// code omitted
scrolling = controller.scrollTo();

void onDataChanged() async {
  await scrolling
  reload();
}

the reason is:

  1. the AutoScrollTag will remove itself after state dispose and added when initState.
  2. if the state is not ready to operate with, reseting the discovered state is unhelpful since we can't find it as well.
  3. we shouldn't rebuild when scrollTo() is active.
allComputableThings commented 3 years ago

Thanks. I’ll give it a go.

On Wed, Nov 25, 2020 at 10:42 PM Jerry Chen notifications@github.com wrote:

the straightforward solution seems that you may need a flag to indicate the AutoScrollController is searching. e.g.

var scrolling = false

/// code omit scrolling = true; try { await controller.scrollTo(); } finally { scrolling = false; }

void onDataChanged() { if (scrolling) reloadLater(); else reload() }

the reason is:

  1. the AutoScrollTag will remove itself after state dispose and added when initState.
  2. if the state is not ready to operate with, reseting the discovered state is unhelpful since we can't find it as well.
  3. we shouldn't rebuild when scrollTo() is active.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/quire-io/scroll-to-index/issues/37#issuecomment-734107000, or unsubscribe https://github.com/notifications/unsubscribe-auth/AB3QJLH54EPUCRPDTTZDONDSRX2FLANCNFSM4UCW3I3Q .

allComputableThings commented 3 years ago

It seems this issue happens even when the view is reloaded while not scrolling - (I have asserted for AutoScrollController.isScrolling==false before starting a new scrollToIndex). I've also reduced the scroll duration to 1 seconds to remove the likelihood that a widget might not be built because of fast scrolling.

I/flutter ( 8233): scrollToIndex(30)  -  1039803395   false     #  1039803395 is the AutoScrollController.hashCode
I/flutter ( 8233): Build 34                                      # Item builder's index
I/flutter ( 8233): Register 1039803395 - 34          # AutoScrollTag.register and index
I/flutter ( 8233): Build 36
I/flutter ( 8233): Register 1039803395 - 36
I/flutter ( 8233): Build 38
I/flutter ( 8233): Register 1039803395 - 38
I/flutter ( 8233): Build 40
I/flutter ( 8233): Register 1039803395 - 40
I/flutter ( 8233): Build 42
I/flutter ( 8233): Register 1039803395 - 42
I/flutter ( 8233): Build 44
I/flutter ( 8233): Register 1039803395 - 44
I/flutter ( 8233): Build 45
I/flutter ( 8233): Register 1039803395 - 45
I/flutter ( 8233): Build 47
I/flutter ( 8233): Register 1039803395 - 47
I/flutter ( 8233): Unregister 1039803395 - 2/52       # AutoScrollTag.unregister   index/new tagMap.length
I/flutter ( 8233): Build 49
I/flutter ( 8233): Register 1039803395 - 49
I/flutter ( 8233): Build 50
I/flutter ( 8233): Register 1039803395 - 50
I/flutter ( 8233): Unregister 1039803395 - 4/53
I/flutter ( 8233): Build 51
I/flutter ( 8233): Register 1039803395 - 51
I/flutter ( 8233): Build 53
I/flutter ( 8233): Register 1039803395 - 53
I/flutter ( 8233): Unregister 1039803395 - 6/54
I/flutter ( 8233): Build 54
I/flutter ( 8233): Register 1039803395 - 54
I/flutter ( 8233): Unregister 1039803395 - 10/54
I/flutter ( 8233): Unregister 1039803395 - 8/53
I/flutter ( 8233): Unregister 1039803395 - 11/52
I/flutter ( 8233): Build 56
I/flutter ( 8233): Register 1039803395 - 56
I/flutter ( 8233): Unregister 1039803395 - 13/52
I/flutter ( 8233): Build 57
I/flutter ( 8233): Register 1039803395 - 57
I/flutter ( 8233): Unregister 1039803395 - 14/52
I/flutter ( 8233): Build 58
I/flutter ( 8233): Register 1039803395 - 58
I/flutter ( 8233): Unregister 1039803395 - 15/52
I/flutter ( 8233): Build 59
I/flutter ( 8233): Register 1039803395 - 59
I/flutter ( 8233): Unregister 1039803395 - 17/52
I/flutter ( 8233): scrollToIndex(14)  -  1039803395   false 
E/flutter ( 8233): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: 'package:scroll_to_index/scroll_to_index.dart': Failed assertion: line 339 pos 14: '(offsetToLastState?.offset ?? 0) >= 0': ERROR: %%%%  1039803395 tagMaplen:52 %%%%%%%%%%: 14, 0, 1.0, RevealedOffset(offset: -408.8262626669658, rect: Rect.fromLTRB(NaN, NaN, NaN, NaN)), 0,1,3,5,7,9,12,16,18,20,23,24,25,26,27,28,29,30,31,32,33,35,37,39,41,43,46,48,52,55,61,65,22,21,19,34,36,38,40,42,44,45,47,49,50,51,53,54,56,57,58,59

In the exception, I'm printing the tagMap keys. scrollToIndex(14) is failing - I assume because 14 is not in the list - it was recently unregistered because the widget was offscreen and disposed.

allComputableThings commented 3 years ago

I think there is a bug with the way offset are calculated.

I believe I was able to resolve it with the following changes:

/// Replace:
  int _getNearestIndex(int index) {
    final list = tagMap.keys;
    if (list.isEmpty)
      return null;

    int sortkey(int i)=>(index-i).abs();

    final sorted = list.toList()..sort((int first, int second) => sortkey(first).compareTo(sortkey(second)));
    final min = sorted.first;
    // final max = sorted.last;
    return min;
    // return (index - min).abs() < (index - max).abs() ? min : max;
  }

---       assert((offsetToLastState?.offset ?? 0) >= 0,
      "ERROR: %%%%%%%%%%%%%%: $targetIndex, $currentNearestIndex, $alignment, $offsetToLastState, ${tagMap.keys.toList().join(',')}");
allComputableThings commented 3 years ago

I'm unsure why I can't find the conditions that cause this to happen though -- I haven't been able to make a good reproduction.

jerrywell commented 3 years ago

the min/max search is trying to get the nearest position to scroll.

you can still try to make a promising reproduce sample after then. at least, the assertion in production won't throw exception. in my experience, it may be a bug from flutter's sliver mechanism sometimes.

allComputableThings commented 3 years ago

_getNearestIndex returns only the highest or lowest key in the tagMap: https://github.com/quire-io/scroll-to-index/blob/306ec2f63a91a99b4ac873e470b546f34cfac5d4/lib/scroll_to_index.dart it is a bug. (My code above finds the nearest).

Something else funky is also happening. For example, in the example above there are many gaps in tagMap keys:

0,1,3,5,7,9,12,16,18,20,23,24,25,26,27,28,29,30,31,32,33,35,37,39,41,43,46,48,52,55,61,65,22,21,19,34,36,38,40,42,44,45,47,49,50,51,53,54,56,57,58,59

4? 52? Where you at?

jerrywell commented 3 years ago

can you provide the reproduce example. otherwise, I can help nothing.

wph144 commented 3 years ago

Same error happend to me. In my case, 'addPostFrameCallback' helps me.

TextButton(
  onPressed: () async {
    itemList.add(itemList.length + 1);
    setState(() {});
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      if (mounted) {
        scrollController.scrollToIndex(itemList.length);
      }
    });
  },
  child: Text('ADD item')
)