Skip to content

Commit bdcb716

Browse files
committed
fix: #113
Defer observation until frame completion to ensure accurate result.
1 parent f11b7b3 commit bdcb716

File tree

4 files changed

+80
-16
lines changed

4 files changed

+80
-16
lines changed

lib/src/common/observer_widget.dart

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -317,27 +317,32 @@ class ObserverWidgetState<
317317
.contains(notification.runtimeType)) {
318318
final isIgnoreInnerCanHandleObserve =
319319
ScrollUpdateNotification != notification.runtimeType;
320-
final platform = Theme.of(context).platform;
321-
if (kIsWeb ||
322-
platform == TargetPlatform.windows ||
323-
platform == TargetPlatform.macOS) {
320+
WidgetsBinding.instance.endOfFrame.then((_) {
321+
// Need to wait for frame end to avoid inaccurate observation
322+
// result, reasons as follows
323+
//
324+
// ======================== WEB ========================
325+
//
324326
// Getting bad observation result because scrolling in Flutter Web
325327
// with mouse wheel is not smooth.
326328
// https://github.com/flutter/flutter/issues/78708
327329
// https://github.com/flutter/flutter/issues/78634
328330
//
329331
// issue
330332
// https://github.com/LinXunFeng/flutter_scrollview_observer/issues/31
331-
WidgetsBinding.instance.addPostFrameCallback((_) {
332-
handleContexts(
333-
isIgnoreInnerCanHandleObserve: isIgnoreInnerCanHandleObserve,
334-
);
335-
});
336-
} else {
333+
//
334+
// ======================== APP ========================
335+
//
336+
// When using ScrollController's animateTo with a value exceeding
337+
// the maximum scroll range, it will lead to inaccurate
338+
// observation result.
339+
//
340+
// issue
341+
// https://github.com/fluttercandies/flutter_scrollview_observer/issues/113
337342
handleContexts(
338343
isIgnoreInnerCanHandleObserve: isIgnoreInnerCanHandleObserve,
339344
);
340-
}
345+
});
341346
}
342347
return false;
343348
},
@@ -449,7 +454,9 @@ class ObserverWidgetState<
449454
bool isIgnoreInnerCanHandleObserve = true,
450455
}) {
451456
if (!isIgnoreInnerCanHandleObserve) {
452-
if (!innerCanHandleObserve) return null;
457+
if (!innerCanHandleObserve) {
458+
return null;
459+
}
453460
updateInnerCanHandleObserve();
454461
}
455462

@@ -467,7 +474,9 @@ class ObserverWidgetState<
467474

468475
final isHandlingScroll =
469476
widget.sliverController?.innerIsHandlingScroll ?? false;
470-
if (isHandlingScroll) return null;
477+
if (isHandlingScroll) {
478+
return null;
479+
}
471480

472481
List<BuildContext> ctxs = fetchTargetSliverContexts();
473482

test/grid_observer_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ void main() {
9292
curve: Curves.easeInOut,
9393
);
9494
await tester.pumpAndSettle();
95+
await tester.pump(observerController.observeIntervalForScrolling);
9596
expect(pageController.page, 3);
9697
expect(isCalledOnObserve, isFalse);
9798

@@ -100,6 +101,8 @@ void main() {
100101
duration: const Duration(milliseconds: 100),
101102
curve: Curves.easeInOut,
102103
);
104+
await tester.pumpAndSettle();
105+
await tester.pump(observerController.observeIntervalForScrolling);
103106
expect(isCalledOnObserve, isTrue);
104107

105108
scrollController.dispose();

test/list_observer_test.dart

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ import 'package:scrollview_observer/src/common/observer_widget_tag_manager.dart'
66

77
void main() {
88
Widget getListView({
9+
ScrollPhysics? physics,
910
ScrollController? scrollController,
1011
int itemCount = 100,
1112
bool isFixedHeight = false,
1213
double? cacheExtent,
1314
NullableIndexedWidgetBuilder? itemBuilder,
15+
IndexedWidgetBuilder? separatorBuilder,
1416
}) {
1517
return Directionality(
1618
textDirection: TextDirection.ltr,
1719
child: ListView.separated(
20+
physics: physics,
1821
controller: scrollController,
1922
itemBuilder: (ctx, index) {
2023
if (itemBuilder != null) {
@@ -31,9 +34,10 @@ void main() {
3134
),
3235
);
3336
},
34-
separatorBuilder: (ctx, index) {
35-
return const SizedBox(height: 10);
36-
},
37+
separatorBuilder: separatorBuilder ??
38+
(ctx, index) {
39+
return const SizedBox(height: 10);
40+
},
3741
itemCount: itemCount,
3842
cacheExtent: cacheExtent,
3943
),
@@ -131,6 +135,7 @@ void main() {
131135
curve: Curves.easeInOut,
132136
);
133137
await tester.pumpAndSettle();
138+
await tester.pump(observerController.observeIntervalForScrolling);
134139
expect(pageController.page, 3);
135140
expect(isCalledOnObserve, isFalse);
136141

@@ -139,12 +144,56 @@ void main() {
139144
duration: const Duration(milliseconds: 100),
140145
curve: Curves.easeInOut,
141146
);
147+
await tester.pumpAndSettle();
148+
await tester.pump(observerController.observeIntervalForScrolling);
142149
expect(isCalledOnObserve, isTrue);
143150

144151
scrollController.dispose();
145152
pageController.dispose();
146153
});
147154

155+
testWidgets('Check observation result after fast scrolling', (tester) async {
156+
// Regression test for https://github.com/fluttercandies/flutter_scrollview_observer/issues/113
157+
final scrollController = ScrollController();
158+
final observerController = ListObserverController(
159+
controller: scrollController,
160+
);
161+
const int itemCount = 50;
162+
Widget widget = getListView(
163+
scrollController: scrollController,
164+
physics: const ClampingScrollPhysics(),
165+
itemCount: itemCount,
166+
itemBuilder: (context, index) => Text('$index'),
167+
separatorBuilder: (context, index) => const SizedBox.shrink(),
168+
);
169+
int? lastItemIndex;
170+
widget = ListViewObserver(
171+
child: widget,
172+
controller: observerController,
173+
triggerOnObserveType: ObserverTriggerOnObserveType.directly,
174+
onObserve: (result) {
175+
lastItemIndex = result.displayingChildModelList.last.index;
176+
},
177+
);
178+
await tester.pumpWidget(widget);
179+
180+
var result = await observerController.dispatchOnceObserve();
181+
final offset = result.observeResult?.viewport.offset
182+
as ScrollPositionWithSingleContext;
183+
final maxScrollExtent = offset.maxScrollExtent;
184+
scrollController.animateTo(
185+
maxScrollExtent * 1000,
186+
duration: const Duration(milliseconds: 100),
187+
curve: Curves.easeOut,
188+
);
189+
190+
await tester.pumpAndSettle();
191+
await tester.pump(observerController.observeIntervalForScrolling);
192+
expect(lastItemIndex, itemCount - 1);
193+
194+
scrollController.dispose();
195+
});
196+
148197
group('Scroll to index', () {
149198
testWidgets('Dynamic Height', (tester) async {
150199
final scrollController = ScrollController();

test/sliver_observer_test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ void main() {
304304
curve: Curves.easeInOut,
305305
);
306306
await tester.pumpAndSettle();
307+
await tester.pump(observerController.observeIntervalForScrolling);
307308
expect(pageController.page, 3);
308309
expect(isCalledOnObserve, isFalse);
309310

@@ -312,6 +313,8 @@ void main() {
312313
duration: const Duration(milliseconds: 100),
313314
curve: Curves.easeInOut,
314315
);
316+
await tester.pumpAndSettle();
317+
await tester.pump(observerController.observeIntervalForScrolling);
315318
expect(isCalledOnObserve, isTrue);
316319

317320
scrollController.dispose();

0 commit comments

Comments
 (0)