Default `PrimaryScrollController` on Desktop
Summary
#
                  The PrimaryScrollController API has been updated to no longer automatically
                  attach to vertical ScrollViews on desktop platforms.
                
Context
#
                  Prior to this change, ScrollView.primary would default to true if a
                  ScrollView had an Axis.vertical scroll direction and a ScrollController
                  
                  had not already been provided. This allowed for common UI patterns, like the
                  scroll-to-top function on iOS to work out of the box for Flutter apps.
                  On desktop however, this default would often cause the following assertion error:
                
ScrollController attached to multiple ScrollViews.
                    
                    
                    
                  
                  While it is common for a mobile application to display one ScrollView at a time,
                  desktop UI patterns are more likely to display multiple ScrollViews
                  side-by-side. The prior implementation of PrimaryScrollController conflicted
                  with this pattern, resulting in an often unhelpful error message. To remedy this,
                  the PrimaryScrollController has been updated with additional parameters as
                  well as better error messaging across multiple widgets that depend on it.
                
Description of change
#
                  The previous implementation of ScrollView resulted in primary being true by
                  default for all vertical ScrollViews that did not already have a
                  ScrollController, on all platforms. This default behavior was not always clear,
                  particularly because it is separate from the PrimaryScrollController itself.
                
// Previously, this ListView would always result in primary being true,
// and attached to the PrimaryScrollController on all platforms.
Scaffold(
  body: ListView.builder(
    itemBuilder: (BuildContext context, int index) {
      return Text('Item $index');
    }
  ),
);
                    
                    
                    
                  
                  The implementation changes ScrollView.primary to be nullable, with the fallback
                  decision-making being relocated to the PrimaryScrollController.
                  When primary is null, and no ScrollController has been provided, the 
                  ScrollView
                  will look up the PrimaryScrollController and instead call shouldInherit
                   to
                  determine if the given ScrollView should use the PrimaryScrollController.
                
                  The new members of the PrimaryScrollController class,
                  automaticallyInheritForPlatforms and scrollDirection, are evaluated in
                  shouldInherit, allowing users clarity and control over the
                  PrimaryScrollController's behavior.
                
                  By default, backwards compatibility is maintained for mobile platforms.
                  PrimaryScrollController.shouldInherit returns true for vertical
                  ScrollViews. On desktop, this returns false by default.
                
// Only on mobile platforms will this attach to the PrimaryScrollController by
// default.
Scaffold(
  body: ListView.builder(
    itemBuilder: (BuildContext context, int index) {
      return Text('Item $index');
    }
  ),
);
                    
                    
                    
                  
                  To change the default, users can set ScrollView.primary true or false to
                  explicitly manage the PrimaryScrollController for an individual ScrollView.
                  For behavior across multiple ScrollViews, the PrimaryScrollController
                   is now
                  configurable by setting the specific platform, as well as the scroll direction
                  that is preferred for inheritance.
                
                  Widgets that use the PrimaryScrollController, such as NestedScrollView,
                  Scrollbar, and DropdownMenuButton will experience no change to existing
                  functionality. Features like the iOS scroll-to-top will also continue to work as
                  expected without any migration.
                
                  ScrollActions, and ScrollIntents on desktop are the only classes affected by
                  this change, requiring migration. By default, the PrimaryScrollController
                   is
                  used to execute fallback keyboard scrolling Shortcuts if the current 
                  Focus is
                  contained within a Scrollable. Since displaying more than one ScrollView
                  
                  side-by-side is common on desktop platforms, it isn't possible for
                  Flutter to decide "Which ScrollView should be primary in this view and receive
                  the keyboard scroll action?"
                
                  If more than one ScrollView was present previous to this change, the same
                  assertion (ScrollController attached to multiple ScrollViews.) would be thrown.
                  Now, on desktop platforms, users need to specify primary: true to
                  designate which ScrollView is the fallback to receive unhandled keyboard
                  Shortcuts.
                
Migration guide
#Code before migration:
// These side-by-side ListViews would throw errors from Scrollbars and
// ScrollActions previously due to the PrimaryScrollController.
Scaffold(
  body: LayoutBuilder(
    builder: (context, constraints) {
      return Row(
        children: [
          SizedBox(
            height: constraints.maxHeight,
            width: constraints.maxWidth / 2,
            child: ListView.builder(
              itemBuilder: (BuildContext context, int index) {
                return Text('List 1 - Item $index');
              }
            ),
          ),
          SizedBox(
            height: constraints.maxHeight,
            width: constraints.maxWidth / 2,
            child: ListView.builder(
              itemBuilder: (BuildContext context, int index) {
                return Text('List 2 - Item $index');
              }
            ),
          ),
        ]
      );
    },
  ),
);
                    
                    
                    
                  Code after migration:
// These side-by-side ListViews will no longer throw errors, but for
// default ScrollActions, one will need to be designated as primary.
Scaffold(
  body: LayoutBuilder(
    builder: (context, constraints) {
      return Row(
        children: [
          SizedBox(
            height: constraints.maxHeight,
            width: constraints.maxWidth / 2,
            child: ListView.builder(
              // This ScrollView will use the PrimaryScrollController
              primary: true,
              itemBuilder: (BuildContext context, int index) {
                return Text('List 1 - Item $index');
              }
            ),
          ),
          SizedBox(
            height: constraints.maxHeight,
            width: constraints.maxWidth / 2,
            child: ListView.builder(
              itemBuilder: (BuildContext context, int index) {
                return Text('List 2 - Item $index');
              }
            ),
          ),
        ]
      );
    },
  ),
);
                    
                    
                    
                  Timeline
#
                  Landed in version: 3.3.0-0.0.pre
                  In stable release: 3.3
                
References
#API documentation:
Design document:
Relevant issues:
Relevant PRs:
Unless stated otherwise, the documentation on this site reflects Flutter 3.35.5. Page last updated on 2025-10-28. View source or report an issue.