Fix: Assertion failure in RawScrollbarState with dynamic controller assignment (#173156)

Fix: Assertion failure in RawScrollbarState with dynamic controller
assignment
fixes: #172614 

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
This commit is contained in:
Kishan Rathore 2025-09-03 01:55:08 +05:30 committed by GitHub
parent e0bbb2ff05
commit 433e543260
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 83 additions and 0 deletions

View File

@ -1456,6 +1456,14 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
} else if (_effectiveScrollController != null && enableGestures) {
// Interactive scrollbars need to be properly configured. If it is visible
// for interaction, ensure we are set up properly.
// Don't assert immediately if we're in the middle of updating the widget
// as the controller may not be attached yet in that frame.
if (_fadeoutAnimationController.status == AnimationStatus.forward &&
(widget.thumbVisibility ?? false)) {
// When thumbVisibility is true and we're animating forward,
// the check is already scheduled by _debugScheduleCheckHasValidScrollPosition.
return;
}
assert(_debugCheckHasValidScrollPosition());
}
}

View File

@ -3635,4 +3635,79 @@ The provided ScrollController cannot be shared by multiple ScrollView widgets.''
expect(size.height - rtlRect1.bottom, 4);
expect(rtlRect1.left, 3);
});
testWidgets(
'RawScrollbar does not assert when dynamically assigning controller with thumbVisibility true',
(WidgetTester tester) async {
// This test reproduces the issue from https://github.com/flutter/flutter/issues/172614
// where dynamically assigning a controller and setting thumbVisibility to true
// in the same frame caused an assertion failure.
final ScrollController scrollController = ScrollController();
addTearDown(scrollController.dispose);
bool useController = false;
bool thumbVisible = false;
bool interactive = false;
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Column(
children: <Widget>[
ElevatedButton(
onPressed: () {
setState(() {
// This reproduces the exact scenario from the linked issue:
// Initially the controller is null, thumbVisibility is false, and interactive is false.
// Then, in the next frame, the controller is non-null, thumbVisibility is true, and interactive is true.
useController = true;
thumbVisible = true;
interactive = true;
});
},
child: const Text('Enable Scrollbar'),
),
Expanded(
child: RawScrollbar(
controller: useController ? scrollController : null,
thumbVisibility: thumbVisible,
interactive: interactive,
child: SingleChildScrollView(
controller: useController ? scrollController : null,
child: const SizedBox(height: 2000.0, width: 100.0),
),
),
),
],
),
),
);
},
),
);
await tester.pumpAndSettle();
expect(find.byType(RawScrollbar), findsOneWidget);
await tester.tap(find.text('Enable Scrollbar'));
await tester.pumpAndSettle();
// Verify scrollbar is now working properly and doesn't throw an assertion.
final Finder scrollbarFinder = find.byType(RawScrollbar);
expect(scrollbarFinder, findsOneWidget);
expect(scrollController.offset, 0.0);
// Test that scrolling works without assertions.
await tester.drag(find.byType(SingleChildScrollView), const Offset(0.0, -100.0));
await tester.pumpAndSettle();
// Verify the scroll happened.
expect(scrollController.offset, greaterThan(0.0));
},
);
}