The `PrimaryScrollController` API has been updated to no longer automatically attach to vertical `ScrollView`s on desktop platforms.
Original behavior
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:
New behavior
The previous implementation of `ScrollView` resulted in `primary` being true by default for all vertical `ScrollView`s 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.
- Who is affected
- affected by
- Practical impact
- Behaviour or API differences that can break builds or change runtime behaviour.
- What action should I take?
- Follow the official migration guide below, update affected call sites, run `dart fix --apply` where a fix is available, then run your tests.
Before and after (from the official guide)
| 1 | − | // These side-by-side ListViews would throw errors from Scrollbars and |
| 2 | − | // ScrollActions previously due to the PrimaryScrollController. |
| 3 | Scaffold( | |
| 4 | body: LayoutBuilder( | |
| 5 | builder: (context, constraints) { | |
| 6 | return Row( | |
| 7 | children: [ | |
| 8 | SizedBox( | |
| 9 | height: constraints.maxHeight, | |
| 10 | width: constraints.maxWidth / 2, | |
| 11 | child: ListView.builder( | |
| 12 | itemBuilder: (BuildContext context, int index) { | |
| 13 | return Text('List 1 - Item $index'); | |
| 14 | } | |
| 15 | ), | |
| 16 | ), | |
| 17 | SizedBox( | |
| 18 | height: constraints.maxHeight, | |
| 19 | width: constraints.maxWidth / 2, | |
| 20 | child: ListView.builder( | |
| 21 | itemBuilder: (BuildContext context, int index) { | |
| 22 | return Text('List 2 - Item $index'); | |
| 23 | } | |
| 24 | ), | |
| 25 | ), | |
| 26 | ] | |
| 27 | ); | |
| 28 | }, | |
| 29 | ), | |
| 30 | ); |
| 1 | + | // These side-by-side ListViews will no longer throw errors, but for |
| 2 | + | // default ScrollActions, one will need to be designated as primary. |
| 3 | Scaffold( | |
| 4 | body: LayoutBuilder( | |
| 5 | builder: (context, constraints) { | |
| 6 | return Row( | |
| 7 | children: [ | |
| 8 | SizedBox( | |
| 9 | height: constraints.maxHeight, | |
| 10 | width: constraints.maxWidth / 2, | |
| 11 | child: ListView.builder( | |
| 12 | + | // This ScrollView will use the PrimaryScrollController |
| 13 | + | primary: true, |
| 14 | itemBuilder: (BuildContext context, int index) { | |
| 15 | return Text('List 1 - Item $index'); | |
| 16 | } | |
| 17 | ), | |
| 18 | ), | |
| 19 | SizedBox( | |
| 20 | height: constraints.maxHeight, | |
| 21 | width: constraints.maxWidth / 2, | |
| 22 | child: ListView.builder( | |
| 23 | itemBuilder: (BuildContext context, int index) { | |
| 24 | return Text('List 2 - Item $index'); | |
| 25 | } | |
| 26 | ), | |
| 27 | ), | |
| 28 | ] | |
| 29 | ); | |
| 30 | }, | |
| 31 | ), | |
| 32 | ); |
Raw before / after
// 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');
}
),
),
]
);
},
),
);// 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');
}
),
),
]
);
},
),
);Official migration guide (excerpt, © The Flutter Authors, CC BY 3.0)
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');
}
),
),
]
);
},
),
);AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.Listed in the official breaking-changes index (docs.flutter.dev/release/breaking-changes).
An official announcement or migration guide documents an intended behaviour or API change. A verified source change (git diff) shows code changed but does not by itself prove a public API is breaking. AI interpretations are labelled and never used to classify a change.
- Official migration guide
- PrimaryScrollController
- ScrollView
- ScrollAction
- ScrollIntent
- Updating PrimaryScrollController
- Issue #100264
- Updating PrimaryScrollController for Desktop
- Source commit
- b3045439b7c0
- Source last updated
- Oct 21, 2026
- Last synchronized
- Oct 21, 2026
- Publication status
- published
Publication history
- Oct 21, 2026, 8:31 PM: new → published (ingestion) — Backed by an official breaking-change/migration guide.
AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.