diff --git a/packages/flutter/lib/src/widgets/scrollbar.dart b/packages/flutter/lib/src/widgets/scrollbar.dart index 76891b95ff93..f3387d788b92 100644 --- a/packages/flutter/lib/src/widgets/scrollbar.dart +++ b/packages/flutter/lib/src/widgets/scrollbar.dart @@ -1466,18 +1466,24 @@ class RawScrollbarState extends State with TickerProv ErrorHint( 'The Scrollbar attempted to use the $controllerForError. This ' 'ScrollController should be associated with the ScrollView that ' - 'the Scrollbar is being applied to.' - '${tryPrimary - ? 'When ScrollView.scrollDirection is Axis.vertical on mobile ' - 'platforms will automatically use the ' - 'PrimaryScrollController if the user has not provided a ' - 'ScrollController. To use the PrimaryScrollController ' - 'explicitly, set ScrollView.primary to true for the Scrollable ' - 'widget.' - : 'When providing your own ScrollController, ensure both the ' - 'Scrollbar and the Scrollable widget use the same one.' - }', + 'the Scrollbar is being applied to.', ), + if (tryPrimary) ...[ + ErrorHint( + 'If a ScrollController has not been provided, the ' + 'PrimaryScrollController is used by default on mobile platforms ' + 'for ScrollViews with an Axis.vertical scroll direction.', + ), + ErrorHint( + 'To use the PrimaryScrollController explicitly, ' + 'set ScrollView.primary to true on the Scrollable widget.', + ), + ] + else + ErrorHint( + 'When providing your own ScrollController, ensure both the ' + 'Scrollbar and the Scrollable widget use the same one.', + ), ]); } return true; @@ -1491,26 +1497,32 @@ class RawScrollbarState extends State with TickerProv } throw FlutterError.fromParts([ ErrorSummary( - 'The $controllerForError is currently attached to more than one ' - 'ScrollPosition.', + 'The $controllerForError is attached to more than one ScrollPosition.', ), ErrorDescription( 'The Scrollbar requires a single ScrollPosition in order to be painted.', ), ErrorHint( 'When $when, the associated ScrollController must only have one ' - 'ScrollPosition attached.' - '${tryPrimary - ? 'If a ScrollController has not been provided, the ' - 'PrimaryScrollController is used by default on mobile platforms ' - 'for ScrollViews with an Axis.vertical scroll direction. More ' - 'than one ScrollView may have tried to use the ' - 'PrimaryScrollController of the current context. ' - 'ScrollView.primary can override this behavior.' - : 'The provided ScrollController must be unique to one ' - 'ScrollView widget.' - }', + 'ScrollPosition attached.', ), + if (tryPrimary) ...[ + ErrorHint( + 'If a ScrollController has not been provided, the ' + 'PrimaryScrollController is used by default on mobile platforms ' + 'for ScrollViews with an Axis.vertical scroll direction.' + ), + ErrorHint( + 'More than one ScrollView may have tried to use the ' + 'PrimaryScrollController of the current context. ' + 'ScrollView.primary can override this behavior.' + ), + ] + else + ErrorHint( + 'The provided ScrollController cannot be shared by multiple ' + 'ScrollView widgets.' + ), ]); } return true; diff --git a/packages/flutter/test/material/scrollbar_test.dart b/packages/flutter/test/material/scrollbar_test.dart index a3c1884c69a7..a1fd4ad891e6 100644 --- a/packages/flutter/test/material/scrollbar_test.dart +++ b/packages/flutter/test/material/scrollbar_test.dart @@ -1810,7 +1810,12 @@ void main() { FlutterError error = tester.takeException() as FlutterError; expect( error.message, - contains('The PrimaryScrollController is currently attached to more than one ScrollPosition.'), + ''' +The PrimaryScrollController is attached to more than one ScrollPosition. +The Scrollbar requires a single ScrollPosition in order to be painted. +When Scrollbar.thumbVisibility is true, the associated ScrollController must only have one ScrollPosition attached. +If a ScrollController has not been provided, the PrimaryScrollController is used by default on mobile platforms for ScrollViews with an Axis.vertical scroll direction. +More than one ScrollView may have tried to use the PrimaryScrollController of the current context. ScrollView.primary can override this behavior.''', ); // Asserts when using the ScrollController provided by the user. @@ -1829,7 +1834,11 @@ void main() { error = tester.takeException() as FlutterError; expect( error.message, - contains('The provided ScrollController is currently attached to more than one ScrollPosition.'), + ''' +The provided ScrollController is attached to more than one ScrollPosition. +The Scrollbar requires a single ScrollPosition in order to be painted. +When Scrollbar.thumbVisibility is true, the associated ScrollController must only have one ScrollPosition attached. +The provided ScrollController cannot be shared by multiple ScrollView widgets.''', ); scrollController.dispose(); diff --git a/packages/flutter/test/widgets/scrollbar_test.dart b/packages/flutter/test/widgets/scrollbar_test.dart index 746d2e146ad3..4b4dc52cd501 100644 --- a/packages/flutter/test/widgets/scrollbar_test.dart +++ b/packages/flutter/test/widgets/scrollbar_test.dart @@ -1514,7 +1514,63 @@ void main() { expect(exception, isAssertionError); expect( exception.message, - contains("The Scrollbar's ScrollController has no ScrollPosition attached."), + ''' +The Scrollbar's ScrollController has no ScrollPosition attached. +A Scrollbar cannot be painted without a ScrollPosition. +The Scrollbar attempted to use the PrimaryScrollController. This ScrollController should be associated with the ScrollView that the Scrollbar is being applied to. +If a ScrollController has not been provided, the PrimaryScrollController is used by default on mobile platforms for ScrollViews with an Axis.vertical scroll direction. +To use the PrimaryScrollController explicitly, set ScrollView.primary to true on the Scrollable widget.''', + ); + }); + + testWidgets('Scrollbars assert on multiple scroll positions', (WidgetTester tester) async { + final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: PrimaryScrollController( + controller: scrollController, + child: Row( + children: [ + RawScrollbar( + controller: scrollController, + child: const SingleChildScrollView( + child: SizedBox(width: 10.0, height: 4000.0), + ), + ), + RawScrollbar( + controller: scrollController, + child: const SingleChildScrollView( + child: SizedBox(width: 10.0, height: 4000.0), + ), + ), + ], + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + AssertionError? exception = tester.takeException() as AssertionError?; + // The scrollbar is not visible and cannot be interacted with, so no assertion. + expect(exception, isNull); + // Scroll to trigger the scrollbar to come into view. + final Finder scrollViews = find.byType(SingleChildScrollView); + final TestGesture gesture = await tester.startGesture(tester.getCenter(scrollViews.first)); + await gesture.moveBy(const Offset(0.0, -20.0)); + exception = tester.takeException() as AssertionError; + expect(exception, isAssertionError); + expect( + exception.message, + ''' +The provided ScrollController is attached to more than one ScrollPosition. +The Scrollbar requires a single ScrollPosition in order to be painted. +When the scrollbar is interactive, the associated ScrollController must only have one ScrollPosition attached. +The provided ScrollController cannot be shared by multiple ScrollView widgets.''', ); }); @@ -2892,7 +2948,7 @@ void main() { expect(scrollController.offset, greaterThan(lastPosition)); }); - testWidgets('The bar support mouse wheel event', (WidgetTester tester) async { + testWidgets('The bar supports mouse wheel event', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/pull/109659 final ScrollController scrollController = ScrollController(); addTearDown(scrollController.dispose);