[MUSIC PLAYING]
GREG SPENCER: Hi.
I'm Greg Spencer, software engineer
on the Flutter Framework Team.
CRAIG LABENZ: And I'm Craig Labenz, developer relations
engineer on the Flutter Team.
GREG SPENCER: Hey, Craig.
CRAIG LABENZ: Hey, Greg.
BOTH: I love it when that happens.
Jinx!
CRAIG LABENZ: Do you think they sit us next to each other
because we do that all the time?
GREG SPENCER: Maybe.
But there's also our secret handshake,
plus all the notes we pass.
CRAIG LABENZ: Hm, good point.
GREG SPENCER: And all your Flutter
questions I help answer.
CRAIG LABENZ: Right.
Speaking of, I'm working on a Flutter desktop app,
and I know you implemented a lot of the keyboard shortcuts
and focus work.
Mind if I ask you a few questions?
GREG SPENCER: Yeah, I think I've got time.
CRAIG LABENZ: Great.
I've been making mobile Flutter apps for years now,
but some of these desktop considerations are new to me.
GREG SPENCER: We're entering a whole new world,
targeting six major platforms.
Which desktop features do you need help with?
CRAIG LABENZ: Some friends and I have been
building this music player app.
And our users were shocked to find
that it didn't have many keyboard shortcuts,
and pressing Tab to focus items didn't move through the UI
how they expected.
CRAIG LABENZ: Oh no.
Well, a lot of that stuff works right out-of-the-box with
Flutter.
But if your needs are specialized,
you may require our dedicated widgets.
Let's start with focus.
How much do you know about how that works?
CRAIG LABENZ: Mm, not a ton.
I know I can create my own focus nodes, but I almost never do.
GREG SPENCER: That's usually enough for mobile apps,
but you're aiming bigger now.
Sprinkled throughout your widget tree are lots of focus widgets
that manage those focus nodes.
And, collectively, they form their own secret tree,
the focus tree.
CRAIG LABENZ: Sounds mysterious.
GREG SPENCER: Well, hopefully, it won't be for long.
These focus widgets appear any time
you have a button, text input, or, well, anything that's
interactive, really, and their entire purpose
is to tell your app how to interpret keypress events.
CRAIG LABENZ: That makes sense.
It's easy to think of only text inputs as being focusable.
But, for accessibility reasons, I
guess everything interactive should be
controllable via the keyboard.
GREG SPENCER: Yeah, exactly.
Now widgets don't know how to magically indicate
when they're focused, but Material widgets
pay attention to their inner focus widget,
listen for updates, and reflect the updates visually.
CRAIG LABENZ: Oh, so that explains
why pressing Tab seemed to do a lot before I wrote any code.
But the order the focus took through my app
wasn't what I wanted.
GREG SPENCER: Oh, can you show me what it was doing?
CRAIG LABENZ: Of course.
We have a navigation rail on the left,
and focus started in the upper corner, just like I expected.
But then halfway down the list, it
detours into the main area for some other navigation tabs,
then jumps back and finishes the navigation rail.
GREG SPENCER: Oh, I see.
That behavior might not have been what you wanted,
but it is what I would expect until you give the focus
tree some instructions.
CRAIG LABENZ: You expected this?
GREG SPENCER: Yeah.
Flutter's default behavior is not
to traverse the focus nodes based
on where they appear in your widget tree,
but instead to consider the geometry of where each element
appears on the screen.
The default traversal order is based
on reading order, which is how focus
knew to start in the upper corner
and work its way down your navigation rail.
CRAIG LABENZ: That makes sense.
But then why did it jump into my row of navigation tabs
halfway down?
GREG SPENCER: Because it read the top items first,
but when it reached the line with your inner tabs,
it started to read all the way across the page.
Since your app currently has left to right directionality,
this behavior tells me that Flutter
didn't find any focus nodes in the main body of your app
until those tabs.
Once it did, it completed that horizontal line
of widgets, then resumed going top to bottom.
CRAIG LABENZ: Oh, this makes so much more sense.
Of course, I don't want it to do this.
So you mentioned something about instructing the focus tree?
GREG SPENCER: Sure.
To specify how focus traversal should work,
there are two common tools at your disposal.
The easier one is simply to wrap your entire navigation rail
in a focus traversal group widget, which
tells Flutter to complete all inner widgets first
before breaking out to the other sections of your UI.
CRAIG LABENZ: Well, I love easy things, so let me try that.
OK, I reloaded it.
And, wow, look at that.
Focus doesn't get distracted and detour
into the navigation tabs.
GREG SPENCER: I guess you could say it stays focused.
CRAIG LABENZ: Greg, yes.
But, say, you also mentioned that there was another way
to instruct the focus tree?
GREG SPENCER: Sure.
The second way is to explicitly set
the order of each individual node in the focus tree.
This is less common, because even within a focus traversal
group, Flutter will, by default, pass focus
around in reading order within the group.
So you have to want a different order to need this.
CRAIG LABENZ: Hm.
My account page has a two-column form,
and I'm thinking that this will help there
GREG SPENCER: That sounds like the type of situation
where specific order is needed.
To do this, set up your FocusTraversalGroup()'s policy
to OrderedTraversalPolicy(), and then wrap each focusable item
with a FocusTraversalOrder widget.
CRAIG LABENZ: Great.
I'll get to that later.
Is there anything important to remember
when unfocusing a widget?
GREG SPENCER: Yes.
Developers often unfocus their widgets
after a task is complete-- for example,
when a form is submitted.
In this scenario, use focusNode.unfocus,
and Flutter will find somewhere else reasonable to put focus.
Just note that something is always focused in your Flutter
app, so it doesn't just outright disappear
when you call unfocus.
If you want to control where Flutter puts the focus,
then don't use unfocus.
Just request focus on the place where you want it to go.
CRAIG LABENZ: Mhm.
Earlier, you mentioned that focus's entire purpose
is to tell Flutter how to interpret
keyboard events, which sounds like the perfect segue
to my users' other request, better keyboard shortcuts.
GREG SPENCER: It is, because keyboard events are always
processed in a context in which widgets have focus.
CRAIG LABENZ: Well, what if I want a global keyboard
shortcut, regardless of where focus is?
For example, in music-playing apps,
pressing Spacebar stops and starts the current song.
I notice that Flutter defaults to having Spacebar
activate whatever element has focus,
but that's not what I want.
GREG SPENCER: Are you sure you always want
Spacebar to toggle playback?
What if the user is searching for a song title and types
a space?
Surely, their song shouldn't pause.
CRAIG LABENZ: OK, that's actually a pretty good point.
Help!
GREG SPENCER: Luckily, Flutter is set up to do what you want.
To start, remember that text inputs
have their own internal focus widget.
So when a user is typing in one, that focus node
will consume all text events, preventing focus nodes higher
up in your widget tree from ever hearing
about those keypress events.
CRAIG LABENZ: I'm guessing this means
I should wrap my entire app in a focus widget that
listens to keypresses.
And, as long as no other focus widget
consumes Spacebar presses, they'll
reach my app-wide listener.
And since I'm not going to add any other widgets that
consume Spacebar presses--
besides the occasional text input--
then all other Spacebar events should reach my global handler.
GREG SPENCER: Yeah, that sounds right.
The easiest way to define keyboard shortcuts is to use
the CallbackShortcuts() widget.
It takes a map of shortcut activators--
basically, key combinations-- and callbacks.
You'll register Spacebar key events
with a function that tells your state management
solution to toggle playback.
CRAIG LABENZ: Wait until my users see this.
And I can presumably use this for all of my global keyboard
shortcuts, like M to toggle Mute?
GREG SPENCER: Exactly.
And it's important to remember, the CallbackShortcuts() widget
works because, internally, it contains a focus widget of its
own.
So it's part of the focus tree.
And since keyboard events propagate up that tree
until something consumes them, if nothing does,
all keyboard events will eventually
arrive at this widget.
CRAIG LABENZ: So handy.
Now you keep mentioning keyboard events getting consumed.
How does that work?
GREG SPENCER: That answer's a bit of a story.
Have you heard "The Life and Times of a Keypress Event"?
CRAIG LABENZ: Um, I don't think so.
Is that an old urban legend?
GREG SPENCER: No, Craig, that was a joke.
CRAIG LABENZ: Oh, uh, I knew that.
GREG SPENCER: We've already talked
about how the focus system's purpose is
to help your app interpret keyboard events.
So it may not be a surprise to learn
that the lifecycle of each keypress
begins at whatever focus node currently has focus.
CRAIG LABENZ: Hm.
Mhm.
GREG SPENCER: Imagine your user has pressed Tab
until a song within a playlist is focused.
You want them to be able to press Control-A for Select All,
maybe for bulk favoriting these songs.
The user presses Control-A, and now we
have an interesting keyboard event to follow.
CRAIG LABENZ: This is so exciting.
GREG SPENCER: I mean, it's a keypress event, but OK.
That event starts bubbling up the tree
until it finds a focus widget whose onkey method handles it.
If it never finds one, the keypress is discarded.
But, in our scenario, they pressed Control-A,
which means something.
CRAIG LABENZ: Now Select All sounds very generic.
I can imagine selecting all songs, playlists, or even
artists, depending on the screen I'm on.
Would this mean this is a good candidate
to be defined at the top of my app?
GREG SPENCER: Yes, precisely.
That's the exact heuristic to keep in mind.
Eventually, this keypress event arrives
at your root-level Shortcuts widget, which matches Control-A
to a Select All intent.
We named it this-- an intent--
because, so far, all your app has worked out
is what generic task the user intends to complete.
You've defined Control-A to mean Select All something,
and the user intends to do that.
But your app doesn't yet know what that something is.
CRAIG LABENZ: I think I see where this is going.
Flutter uses which widget has focus
to work out which something we're selecting all of.
GREG SPENCER: Yes.
Once the keypress is matched to an intent,
Flutter passes the intent like a baton, which starts over back
at the widget that was focused.
The intent works its way up the widget tree,
starting at the focused widget, this time looking
for an actions widget that knows how
to translate the intent into an action, which
is what does the actual work.
CRAIG LABENZ: This really helps me understand when I would
separate my intents from my actions instead of just using
a simple CallbackShortcuts() widget.
If both the intent and the action are specific to that
exact use case, then I can keep things simple with
CallbackShortcuts().
But if a single keyboard shortcut does different things
based on where focus lies, or if multiple keyboard
shortcuts resolve to the same intent,
then it's helpful to define the generic portion
at the top of my app.
GREG SPENCER: Exactly.
Are there any places in your music player
app that you think might use this?
CRAIG LABENZ: Well, one that comes to mind
is the Shuffle button, which appears in a handful of places.
I'd like each one to not only know
how to tell my state management solution to switch
into shuffle mode, but also to respond to Enter presses.
GREG SPENCER: Ah, yes.
But in that scenario, you don't need to do anything special,
because Material buttons already invoke their ontap method
when a user hits the Enter or Spacebar key.
CRAIG LABENZ: Brilliant.
Another area I can think of is controlling the volume slider
and song scrubber with arrow keys.
Both will share the same Shortcuts
and the same intents--
increase intent and decrease intent--
and each widget can then define their own actions
in terms of how to talk to my state management classes.
GREG SPENCER: Yeah, that sounds like a great use case for this.
And if those sliders only appear in your bottom playback bar,
it may be safe to put the Shortcuts
widget around that instead of all the way
at the root of your app.
CRAIG LABENZ: Oh, right.
Yes.
Now there is still one other detail that's unclear to me.
You've described the focus system
as a tree, which implies ancestors and descendants.
But I'm not yet clear on how that's impacted anything.
GREG SPENCER: Oh, I see.
Imagine you have this tree.
CRAIG LABENZ: OK.
I'm imagining it.
GREG SPENCER: If a user presses Tab
until focus arrives at this button,
that widget will be said to have primary focus.
However, in addition to that, any focus nodes
above it in the widget tree will also
have their hasFocus attributes set to true.
And, critically, it's this path up the tree
that key events will follow until they
encounter another Shortcuts widget that matches them
to an intent.
And, by the way, if you're wondering how the Shortcuts
widget does this, it's by leveraging a focus
widget in its own build method.
CRAIG LABENZ: OK.
That was a lot of good info, so let's see if I got everything.
First, the focus system is ready for Tab traversal
across my app on its own without me having to do anything.
But if its default reading order behavior isn't what I want,
I can adjust that with FocusTraversalGroups
or specific focus orders.
GREG SPENCER: Yep, that's all correct.
CRAIG LABENZ: Great.
And, second, the whole point of the focus system
is to help my app interpret keyboard events.
Material widgets already have some sensible defaults
wired up, like focused buttons responding
to Spacebar or Enter.
So, often, I won't have to do anything.
But, if I do, I have two options based
on how generic or specific my actions are.
If my keyboard shortcut is completely scoped to that exact
widget, I can easily use CallbackShortcuts() and define
everything together right where the action is.
But if either of them are generic and shared
across my app, I can use the dedicated Shortcuts widget
and actions widget to avoid constantly redefining
those common parts over and over again.
GREG SPENCER: You got it.
CRAIG LABENZ: We've talked a few times about how
Material widgets behave intelligently by default.
But what would I do if I was writing custom controls?
I've done this in some heavily-branded apps before,
but I was never sure I was doing it right.
GREG SPENCER: Well, for this, I recommend
you look at the build functions of the closest Material
widget you can find, and gloss over all the visual parts.
But, within those build methods, you'll
often find a few interesting widgets to include
in your own custom controls.
CRAIG LABENZ: Copy off the most similar Material widgets--
why didn't I think of that?
GREG SPENCER: It's a great way to get started.
In keeping with our topics, two widgets you'll care about
are actions and focus.
CRAIG LABENZ: I'm not going to lie.
I was sure you were going to include Shortcuts in that list.
GREG SPENCER: Well, you might need that.
But the WidgetsApp, which is also included by MaterialApp,
adds a few generic shortcuts to help you out.
The most helpful ones are both Space and Enter,
which it connects to the very generic intent ActivateIntent.
And if you think back to what we said earlier,
this is perfect, because what Flutter knows at this point
is just that you intend to activate something.
CRAIG LABENZ: You mentioned that the matched intent starts
traversing up the widget tree all over again,
looking for an action.
So I'm guessing that buttons must include an Actions
widget that connects that ActivateIntent to the button's
callback.
GREG SPENCER: That's exactly right.
And this means, to add that functionality
to your own custom controls, as long as your app uses
a WidgetsApp, like MaterialApp or CupertinoApp,
all you have to do is include the actions and focus
parts, knowing that a Shortcuts widget is waiting for you
at the top of your widget tree.
CRAIG LABENZ: Now I'm really seeing
the value in separated actions and shortcuts.
If this trick didn't exist, I'm guessing
that every single control in the Material library
would have had to create an extra copy
of this exact same Shortcuts widget.
GREG SPENCER: They would have.
And this is something you can use in your own apps, too.
Watch for situations where your intent can be interpreted
differently by different actions,
because that may mean you can use something
like ActivateIntent.
Now, if you're building a custom control
and you want complete desktop behavior,
then you need all of this, plus hover highlights.
It would be a lot to add shortcuts, actions, focus,
and mouse region to each control.
In this situation, you can use FocusableActionDetector,
which includes all four for you in the correct order.
There's no getting around that you
will need each of those widgets, but FocusableActionDetector
includes them all in one neat bundle.
CRAIG LABENZ: Amazing.
I can't wait to implement all this.
GREG SPENCER: With these tips, you're
about to have some happy desktop users.
[MUSIC PLAYING]