Skip to content
Boring Doc
BreakingHigh impactOfficial migration guidesince 3.3.0landed 3.3.0-0.0.pre

Default PrimaryScrollController on Desktop

Framework · Official source

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)

dart+4−2
Before
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 );
After
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');
              }
            ),
          ),
        ]
      );
    },
  ),
);
Migration assistantAI-generated · grounded in cited sources
AI explanations are not configured
Set 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.
Official migration guide·Verified classification (95%)

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.

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 explanationAI-generated · grounded in cited sources
AI explanations are not configured
Set 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.
Was this useful?