[MUSIC PLAYING]
KATE LOVETT: One of the most common scenarios in mobile web
and desktop applications is that of having
more content on a given page than can
fit on the user's screen.
The scenario is so common, in fact,
that we mostly take for granted the simple act of flicking
our thumb or rolling the mouse wheel to scroll down
a list of elements.
However, the apparent simplicity of scrolling
through an application's content hides
quite a bit of complexity.
Most of the time, even as app developers,
there's not much interesting to worry about.
Flutter and other frameworks handle the base cases for us
until it is impossible for them to handle
what you want because you want something
a little more specific.
Maybe cards in one of your ListViews are connected,
and clicking on a special icon on one
should scroll to another.
Or maybe like on iOS devices, no matter
how far down you scrolled, you want the user
to be able to return to the top with one tap of the app bar.
And now that Flutter has grown into frequent desktop
usage, which adds more screen real estate
and things like keyboard shortcuts,
matters are even more complicated.
Consider this layout, which is a simplified
version of desktop user interfaces in modern chat apps.
The leftmost column generally holds all of your channels.
The second column may show open threads.
And finally, the largest column holds the current conversation.
If your user presses the page up or page down buttons,
which are common keyboard shortcuts for scrolling,
what should happen, and most interestingly,
how should Flutter know, which brings us
to the matter of ScrollControllers,
the class that, you know, helps you control scrolling.
So where are they, and how do they work?
If you peek at widgets that scroll, like ListView or view,
you'll see an optional parameter-- controller,
of type, ScrollController.
This parameter is optional.
So the first matter is whether or not
ListViews can operate in the complete absence
of a ScrollController.
That answer is a decisive no.
Everything that scrolls requires a controller
to create and manipulate the scroll position.
If we don't supply a ScrollController,
the ListView, which internally is a stateful widget,
will spin up its own.
So are we done here?
If you care about doing something fancy,
you pass a controller.
And if not, you don't.
Not quite.
There's another interesting parameter on ListView,
an optional Boolean called primary,
which plays a big role.
But before we can fully appreciate that parameter,
we have to take a step back and talk
about the PrimaryScrollController.
Remember that iOS scenario we mentioned earlier,
where tapping on the app bar should always return the user
to the top of the page?
That would be very tricky to implement
if your page's main ListView simply created
its own internal scroll controller
and went about its day.
How would its scroll controller ever hear about app bar taps?
The solution here, like with a lot of things in Flutter,
is to lift state up.
Each route widget adds a high-level
PrimaryScrollController widget around its entire subtree,
which is an inherited widget that allows the rest
of your code to call PrimaryScrollController
.ofContext and attain an omnipresent scroll controller.
In a mobile app, this allows Flutter
to add one more thing to the list of features
that just work.
Because list views can subscribe to the PrimaryScrollController,
AppBars can tell it about taps, and the whole system
comes together with Flutter replicating that iOS experience
out of the box.
This pattern has been around in Flutter
since the very beginning, and with the goal
of requiring as few developers as possible
to think about any of this.
The default behavior has always been for vertical ListViews
to make good on this mechanism and get their scroll controller
from the inherited PrimaryScrollController widget.
But there's a catch.
ScrollControllers dislike when multiple scrollable widgets
attach themselves in this way--
like getting invited to two weddings on the same day,
or having your birthday share a major holiday.
Some things just shouldn't happen together.
This almost never came up for the period of Flutter's life
when developers were just making mobile apps.
They rarely have more than one ListView on the screen
at the same time.
But remember that chat app layout?
It has three, and many Flutter developers
were bumping into a gotcha, as they
built desktop apps like this.
When this happens, Flutter would share this delightful note
with you.
"The ScrollController is attached
to multiple scroll views."
This error is particularly fun because most developers
haven't thought about any scroll controllers
whatsoever in their app.
So they naturally have no idea what it's talking about.
It is OK if that it you.
It was me too.
So what do we do?
How do we fix this?
Well, remember that nullable Boolean parameter primary
that I mentioned earlier?
It is finally time to talk about it.
That parameter controls whether or not
the ListView should subscribe to the PrimaryScrollController
or create its own.
Obviously, if you provide your own scroll controller,
then primary must be false.
By the way, primary defaults to true for vertical scrollable
widgets and false for horizontal scrollable widgets,
since that iOS behavior doesn't have an obvious application
for side scrolling.
This is why the error can appear whenever
you have two vertically scrolling widgets
on your screen.
You might be asking yourself, is the default value of True
still working for us?
It's becoming something of a nuisance in desktop and web
applications.
If that's you, there is good news.
In Flutter 3.3 the default value will remain True
for mobile apps to preserve that fancy app bar tap behavior,
but resolve to False on desktop and web
to prevent Flutter developers from being
accosted by this error message.
Do note that when that happens, desktop developers
will need to make sure they identify
which Scrollable should be primary and set
that one to True.
On that note, let's think about the chat app UI some more.
Which ListView makes sense to leave as primary,
and which two should we set to, well, not be primary?
Though desktop and web apps lack that iOS-specific behavior
we discussed earlier, the Page Up and Page Down keyboard
shortcuts create a similar scenario.
You may have guessed it, but you should leave primary set
to True for whichever UI area you want to respond
to default scrolling events.
In this case, probably the main Conversation Panel.
It is worth noting that this system intersects
with Flutter's focus system.
So if your app's focus is located here,
then scrolling events like Page Up
and Page Down will be routed to that ListView,
regardless of which ListView boasts primary status.
For more info on Flutter's focus system,
check out the "Diving Into Desktop" talk
from Google I/O linked below.
Hopefully, this helps you feel less annoyed
by that error message, both because you now
understand what on Earth it's talking about,
and because it'll mostly fade into our distant memories
starting with Flutter 3.3.
For more info on Flutter, head to Flutter.dev.
[MUSIC PLAYING]