Building a desktop design language with Flutter | Session

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

I'm really glad and positively surprised of flutter talks, very high level

👍︎︎ 2 👤︎︎ u/scalatronn 📅︎︎ May 20 2021 đź—«︎ replies
Captions
[MUSIC PLAYING] TODD VOLKERT: Hello, everyone. My name is Todd Volkert, and I'm an engineering manager on the Flutter team. In this session, we're going to talk about building a desktop design language with Flutter. But before we get into the nitty-gritty, let's start by talking about why you might want to build a desktop-specific design language in Flutter. So it's known that Flutter comes with Material or Cupertino or iOS widgets out of the box. This makes it a great choice for building mobile apps and getting rich design components right off the shelf. What you may not have known is that the Material Design Library actually works great on desktop, too. And if I were building a new app from scratch, I would definitely use it. It's performant, beautiful, can adapt to desktop form-factors, and has all the features you'd want out of a widget library straight out of the box. Sometimes, however, your use case calls for a custom UI. Or you might want your app to look like it's using native platform widgets. Maybe it even embeds native platform widgets-- or any number of other reasons that brings you to the conclusion that you kind of need to forge your own path. Also, desktops can be different. The idioms of desktops are so well-established that customers will sometimes have very specific preconceived notions of what their app should look like, and those notions can stretch way outside the bounds of Material Design. And that last point really hits home for me, because I experienced that exact scenario. Here's an app I wrote 14 years ago that's still in use today, but it's being sunset and will soon stop working, so it needs to be reimplemented from scratch. Let's take a quick tour and imagine writing this app in Flutter to see what widgets we would need to build. And right off the bat, we notice that this doesn't look anything like a Material app. It's a dense display, boring, corporate REST app that very much looks like it was written 14 years ago. We have an old-school tab bar for navigation. On the first tab, the user can use their keyboard to quickly navigate around this grid of text inputs. If we try to add a new row to the grid, we're going to need a list button, as well as this pull-down sheet and some stylized buttons. Then we've got buttons that respond to mouse hover. On the second tab, there's a split-pane layout, a selectable list for you on the left, and then a table view on the right that supports lazy loading, sortable columns, resizable columns, and the ability to edit rows inline using arbitrary widgets as cell editors. If we try to add an expense, there's a list button again, but also a calendar button for selecting the date and a spinner widget to copy the expense across days. The third tab contains just a text area. And on the final tab, there are some interesting layouts here-- notably, a scrollable table with fixed column headers and a custom grid pattern. And once the invoice is submitted, the app displays a watermark to show the users that the invoice is now read-only. So this app has been used for 14 years, and its users have expressed a very strong desire to not have to relearn a new user interface. So our task is to write a Flutter app that's a pixel-perfect replica of the existing app so that we can swap it out without our users even noticing. In fact, before we can even attempt to write the app, we need to start by replicating the design language of the app, which brings us back to the point of this talk. So now that we've covered why you might want to go about this, let's take a quick look at how Flutter makes this kind of thing possible. Flutter was designed from the start to have a layered architecture that allows you to hook in at any layer. In fact, the Material and Cupertino libraries are layers built on top of the widget layer. So for our mission, we can choose to disregard the Material and Cupertino libraries and swap out the top layer of the framework with our own package that's also built on top of the widget layer. We'll call this the Chicago design language. OK. Now that we've got the 10,000-foot view, let's go through the process of actually creating one such widget in our target widget set. The widget we'll be working with is a spinner widget. It's relatively simple, but the lessons learned here can then be applied to any number of desktop design widgets. So looking at this widget, how would you go about building it in Flutter? Well, it looks really simple. It looks like you could create a decorated box around a row, where the leftmost entry in the row is the spinner content and the rightmost entry in the row is a column containing the buttons. So here we've attempted that approach, leaving out the decorated box, because that's trivial. And it sort of works, but not really. What happens when you put this in the container that specifies unbounded width constraints? In that case, the expanded widget will force an infinite width, and the row will be unable to lay out. You'll get an exception. For that matter, what happens if you put this in a container that specifies unbounded height constraints? Well, then the cross-axis alignment of stretch will force an infinite height, and you'll get a similar exception. However, if we try to fix that latter problem by adjusting the cross-axis alignment, then the vertical divider disappears. There's also the problem of the horizontal divider between the buttons not showing up. But if we try to fix that by giving the column a cross-axis alignment of stretch, then we get that same old exception again, this time because the column is passed unbounded width constraints by the row. So our attempt to write the spinner widget using simple rows and columns didn't work. What are our options? Well, when it comes to layout, there's often many ways to approach the problem. But whenever you find a widget that you absolutely can't implement using only composition of other widgets, Flutter allows you to get the customization you need by going one layer deeper in the framework. Before we get to coding, let's give a quick roadmap of the areas we'll be working in. You may know that Flutter's widgets are immutable. You can see this in the fact that most widgets have a const constructor, meaning they can be created as compile-time constants. And yet, UIs are very much not immutable. Different parts of your UI will change as the user interacts with it. In fact, generally, your UI will be in constant flux. So if widgets are immutable, but the UI isn't, how does Flutter manage the changing state of the UI? Well, Flutter deals in three parallel trees-- technically, more, but three that are relevant to this discussion. Those trees are the widget tree, which contains widgets, the element tree, which contains elements, and the render tree, which contains-- you guessed it-- render objects. Widgets, as we just covered, are immutable, and simply describe the configuration for elements, which are the actual instantiation of widgets in the tree. Render objects are what actually get laid out and painted on the screen. And correspondingly, they're where layout is implemented. If you're interested in more information about these three trees and how they interact, there's a great TED Talk by Ian Hickson available on YouTube called "The Mahogany Staircase." Now we can get to the code. First let's code up our widget. Rather than StatelessWidget or StatefulWidget, which you're likely already familiar with, we'll create a RenderObjectWidget, which, because it's tied to a render object, will allow us to write our custom layout. This widget takes three child widgets-- the spinner content and the two buttons-- and lays them out according to our specifications. It will then paint the border, and the dividers as well. This means that we'll still need a widget that creates the spinner content and creates the buttons, and then passes them to this raw spinner. We'll come back to that in a little bit. There are two methods we need to implement here, the first of which is createElement, which brings us over to the second of those trees that we talked about. Let's look at the code for the element. Though you never touch them directly, StatelessWidget and StatefulWidget actually have elements, too. They're called, creatively, StatelessElement and StatefulElement. In our case here, we'll be creating a RenderObjectElement, as required by the RenderObjectWidget. Our spinner element will have three child elements to mirror the three child widgets. Since this element has an enumerated set of children, we also create a corresponding enum to represent the slots that those children will occupy in the spinner element. Child models in Flutter framework are opaque. So rather than enumerating our children via public API, we tell the framework how to visit our children. Next up, mounting the spinner element to the element tree will take the child widgets and instantiate or inflate them into the corresponding child elements. Similarly, we wire up what to do when our widget tree has been updated and we have a new widget configuration for this element. You'll notice that this looks exactly the same as what we did in the mount method. That's because the updateChild method that we're calling is smart enough to see whether our child element should be inflated or updated. Now we wire up what to do when we're told to insert a child into this element's render object, and what to do when we're told to remove a child from this element's render object. Note here that we're presupposing the existence of API and a render object class that doesn't exist yet-- namely, the setters for content, up button, and down button. We're going to write that API in just a few seconds. Back in our raw spinner widget, the other method we needed to implement was createRenderObject, which brings us, finally, to our render object, where we'll get to write our custom layout. Back to the code. We start by implementing those API methods that we called from our element class and said we would write in just a few seconds, the setters for the content and the up buttons. And finally, we get to the place where we actually do the layout. Here we're asking the button what its intrinsic width is, or the width that it would like to be, then laying out the content, telling it what range of sizes it's allowed to be while reserving space for the buttons in the border. Once the content's been laid out, we lay the buttons out as well, telling them each to be half the height of our content. Then finally, we set our own size based on the size of our content in the buttons. The render object is also where we ask our children to paint themselves at the appropriate location in our coordinate space, as well as painting anything else that our widget calls for, which in this case is the gradient behind the buttons and the border and the dividers, the code of which is redacted here. OK. We're almost done. Remember how we said we still needed a widget that created the spinner content and created the buttons, and then pass them to our raw spinner widget? Well, now it's time to circle back and create that. It's actually not uncommon to have your public API be a stateful or stateless widget that is composed of one or more private RenderObjectWidgets, and that's the pattern we're using here. We create a controller class that manages the selected index of the spinner and fires events when the selected index changes. And then we define an item builder so that we can build the content of the spinner lazily as the selected index changes. The state object will listen to the controller and manage a corresponding index state variable. And the build method, it will create the two buttons, as well as the content, by invoking the widgets' item builder. Then as planned, it passes those widgets to our raw spinner that we've already coded up. The buttons themselves are simple StatefulWidgets that know how to paint a directional arrow and respond to taps. The button state object maintains a pressed state variable that it manages in the tap event handlers. Its build function leverages the GestureDetector widget to do the heavy lifting, and it uses a custom painter to paint the directional arrows. Phew. That was a lot. Let's take a breath and celebrate that we just got our basic spinner widget working. OK. You ready for the next steps? Let's write it up to handle mouse input and not just touch input. Actually, the gesture detector works both for touch and mouse input. So we're already done with that part. Now onto keyboard handling. This is a little more involved than mouse input. Unlike a mouse cursor, which exists over a specific pixel and so tells the framework where the input should be directed, keyboard input will be directed to the focused widget, which means that in order to receive keyboard input, we need to make our widget focusable. It's worth pointing out that things like focusability are already built in to the widgets that come with Flutter, like those in the Material and Cupertino libraries. But since we're writing our own widget from scratch, we have to worry about these things that would normally be handled for us automatically. It's sometimes said that in Flutter, everything's a widget. And in this case, it bears out. In order to make our widget focusable, we wrap it in a Focus widget. The controller, if you will, for a Focus widget is called a FocusNode. In this case, it will be managed by our spinner state object. And we'll register for focus change events so as to maintain a _isFocused state variable that will then be used to draw our focus indicator. So now our widget can gain keyboard focus, which means that it will be sent key events that occur when our widget has the focus. Again, unlike a mouse, which has a primary button that can be automatically mapped to the tap gesture, there are a great many keys on a keyboard. So we need to tell our widget which key combinations it cares about. For our purposes, we'll tell our spinner to respond to up and down arrow keys. This is as simple as registering an onKey handler with our Focus widget, and then responding to the arrow keys by updating the selected index in our spinner controller. The final bit that's important to add for all widgets is semantic information, to make it understandable by systems designed to improve accessibility, such as screen readers. Just as it was with focus, accessibility support is built in to the widgets that come with the Material and Cupertino libraries. But since we're hand-rolling our own widget here, it's up to us to add that support. This is done with the Semantics widget, which has a ton of properties you can use to describe the semantic meaning of a widget, as well as how certain generic gestures should be handled by the widget. So we just covered the steps required to build one widget. But can we really use that knowledge to build our target design language? Yes. Although there are some advanced concepts beyond the scope of this talk that you would need to learn in order to build some of our widgets, the concepts covered here are all applicable. In fact, this app right here is a Flutter app. It's a kitchen-sink demo of the widgets that have been created as part of the Chicago widget set. It contains all the widgets we would need to build our invoice app in Flutter, even the table views with editable rows. So how about that invoice app? Did we accomplish our mission? Let's have a look. Here you can see before and after versions of the original app versus the reimplemented Flutter app. It looks like we were pretty successful. There's a few minuscule differences in the layouts, which I can go back and fix, but honestly, it kind of helped to leave them in. Otherwise, you might not have believed that these were actually different apps. So hopefully from this talk you've learned that, one, Flutter works well for desktop and web apps; two, Flutter is capable of handling any crazy design you throw at it, even when that crazy design is an old, boring legacy app; and three, how to go about writing your own desktop widgets in Flutter if the need should ever arise. Thank you for staying with me through this talk. If you're interested in digging deeper into the topics we've covered here today, the source code for the Chicago design language is available on GitHub. Also, check out these other tech talks on Flutter at Google I/O.
Info
Channel: Flutter
Views: 51,712
Rating: undefined out of 5
Keywords: purpose: Educate, type: Conference Talk (Full production), pr_pr: Google I/O, flutter widget tutorial, flutter widget 101, build custom widget set, build widget flutter, build Spinner widget, how to build custom widget, how to build widget flutter, desktop design language, build desktop design language, cross-platform, cross platform, crossplatform, #GoogleIO, Google I/O, Google IO, Google developer conference, Google announcement, Google conference, Google
Id: z6PpxO7R0gM
Channel Id: undefined
Length: 15min 13sec (913 seconds)
Published: Wed May 19 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.