Learn about Flutter desktop - read the full article about Flutter, Mobile App Development and Native and cross-platform solutions from Flutter on Qualified.One
[MUSIC PLAYING] GREG SPENCER: Hi.
Im Greg Spencer, software engineer on the Flutter Framework Team.
CRAIG LABENZ: And Im 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 theres 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, Im 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 Ive got time.
CRAIG LABENZ: Great.
Ive been making mobile Flutter apps for years now, but some of these desktop considerations are new to me.
GREG SPENCER: Were 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 didnt have many keyboard shortcuts, and pressing Tab to focus items didnt 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.
Lets 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: Thats usually enough for mobile apps, but youre 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 wont be for long.
These focus widgets appear any time you have a button, text input, or, well, anything thats interactive, really, and their entire purpose is to tell your app how to interpret keypress events.
CRAIG LABENZ: That makes sense.
Its 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 dont know how to magically indicate when theyre 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 wasnt 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.
Flutters 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 didnt 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 dont 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 doesnt 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 Im 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.
Ill 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 doesnt just outright disappear when you call unfocus.
If you want to control where Flutter puts the focus, then dont use unfocus.
Just request focus on the place where you want it to go.
CRAIG LABENZ: Mhm.
Earlier, you mentioned that focuss 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 thats 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 shouldnt pause.
CRAIG LABENZ: OK, thats 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: Im 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, theyll reach my app-wide listener.
And since Im 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.
Youll 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 its important to remember, the CallbackShortcuts() widget works because, internally, it contains a focus widget of its own.
So its 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 answers a bit of a story.
Have you heard "The Life and Times of a Keypress Event"? CRAIG LABENZ: Um, I dont 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: Weve already talked about how the focus systems 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.
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, its 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 Im on.
Would this mean this is a good candidate to be defined at the top of my app? GREG SPENCER: Yes, precisely.
Thats 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.
Youve defined Control-A to mean Select All something, and the user intends to do that.
But your app doesnt 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 were 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 its 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.
Id 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 dont 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.
Now there is still one other detail thats unclear to me.
Youve described the focus system as a tree, which implies ancestors and descendants.
But Im not yet clear on how thats impacted anything.
GREG SPENCER: Oh, I see.
Imagine you have this tree.
CRAIG LABENZ: OK.
Im 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, its 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 youre wondering how the Shortcuts widget does this, its by leveraging a focus widget in its own build method.
CRAIG LABENZ: OK.
That was a lot of good info, so lets 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 isnt what I want, I can adjust that with FocusTraversalGroups or specific focus orders.
GREG SPENCER: Yep, thats 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 wont 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: Weve talked a few times about how Material widgets behave intelligently by default.
But what would I do if I was writing custom controls? Ive 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, youll often find a few interesting widgets to include in your own custom controls.
CRAIG LABENZ: Copy off the most similar Material widgets-- why didnt I think of that? GREG SPENCER: Its a great way to get started.
In keeping with our topics, two widgets youll care about are actions and focus.
CRAIG LABENZ: Im 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 Im guessing that buttons must include an Actions widget that connects that ActivateIntent to the buttons callback.
GREG SPENCER: Thats 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 Im really seeing the value in separated actions and shortcuts.
If this trick didnt exist, Im 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 youre 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.
Theres no getting around that you will need each of those widgets, but FocusableActionDetector includes them all in one neat bundle.
CRAIG LABENZ: Amazing.
I cant wait to implement all this.
GREG SPENCER: With these tips, youre about to have some happy desktop users.
Flutter: Learn about Flutter desktop - Mobile App Development