Pragmatic State Management in Flutter (Google I/O'19)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[MUSIC PLAYING] MATT SULLIVAN: Welcome, everyone, and thank you for coming along to our talk today. We are very happy to see so many people in the audience, because we spent a long time thinking about how we could make the title of our talk the most exciting and click-baity title we could make it. And all we could come up with was pragmatic state management. So thank you for taking the time to come and see our talk. My name is Matt Sullivan. FILIP HRACEK: My name Filip Hracek. We're both developer advocates on the Flutter team. MATT SULLIVAN: Yes, and we're here today to talk about state management and a pragmatic approach to it in Flutter. So why are we here talking about state again? We were here last year talking about state. We've talked about state before. And really, what we want to do is take you through how an app typically gets developed. So usually, apps start off with a very sophisticated product design diagram to tell you how your app is laid out. And this is given to developers, and then the next thing that's expected is you build the app. [LAUGHTER] And you may laugh, but that probably happens more than we'd like to admit. And with Flutter, that's not entirely inconceivable in that with Flutter and Flutter's way of handling composing UIs together, it's relatively straightforward to be able to build out a UI close, if not very, very close, to what your designers produce, which is all well and good. So am I saying Flutter goes from napkin to app immediately? Well, you have this problem, which is interactivity, and how do you handle data flowing through your app, changing state, widgets interacting with other widgets. Getting this wrong at the very beginning is a very costly mistake to make. And so what we want to do is arm you with the tools that you need to be able to make an effective choice at the beginning. Click. There we go. So what are we going to talk about today? We're going to talk about what state management means when we talk about Flutter and why it's really, really important. We're going to give you a pragmatic means of managing your state, but we're going to take you through a journey first. Because we have been trying lots of different ways of handling state here at Google. We've tried several different approaches, and it's taken us a while to get to the point where we're at today. So we're going to give you a little back story, and then we're going to go, and we're, hopefully, going to arm you with some tools to be able to do that. So what do we mean when we say pragmatic? We mean three things. Firstly, your code needs to be understandable and readable, but fundamentally, your UI and your app is going to evolve over time. How you manage state needs to evolve with it. It needs to be flexible and malleable, so you don't end up with a spaghetti mess as you iterate on your UI. Testing is important. And sure, it's easy enough to test state in isolation. But how do you test state when you want to see its effect on UI and vice versa? You need an elegant and easy means to do that. And finally, there is no magical silver bullet here in terms of if you do this, your performance will be awesome every single time. We're going to talk about a means by which you can build in state that will give you good performance, but you always need to be aware of the edge cases. You need to understand where you need to look out for where this may impact your performance. And we'll talk a little bit about that. But first and foremost, what do we mean by state management? FILIP HRACEK: Yes, so I know what you're thinking how can this talk get any more exciting, right? And the answer is definitions. So let's talk about state management. What is state management? Is it the UI state that you manage? Is it the business logic? Is it the network? Is it the bits on the disk? Is it the pixels on the screen? If you think about this long enough, you realize that, basically, all of computer science is you have bits, and you turn them on and off, right? So state management. So are we going to cover all of computer science in the next 30 minutes? MATT SULLIVAN: You've got 35 minutes and 42 seconds. FILIP HRACEK: OK, so let's focus on what most people think about when they ask about state management in Flutter. And what they mean is I have a widget, and I want to change its state from somewhere outside of that widget, either from another widget, or from the network, or a system call. So let's say we have this very simple app. It shows the pie chart, and there is a slider, right? All we want to do is change the state of the pie chart in response to something happening in the slider. If we look at the very simplified widget tree, we have, basically, only three widgets-- my home page and then my slider and my chart. And my slider wants to change the state of my chart. OK, so in this next segment, Matt will pretend to be an inexperienced Flutter developer. And he will try to show you what we often see with inexperienced Flutter developers when they first approach this problem. Take it away. MATT SULLIVAN: This is called Matt writes code and Filip makes fun of it. So in all seriousness, I'm going to try to wire some state management together. Can we flip over to the laptop, please? And we see a lot of people make some of the mistakes that I'm going to do today. So hopefully, this will be instructional in terms of some anti-patterns. And so let's look at what we have. I'm going to start running my app here in my simulator. So basically, what we have is a very simple app. And it has a home page, as Filip pointed out. And it's got a chart, and it's got a slider. We've got some padding and whatnot and some layout. But we effectively have this, and this is great, and nothing really happens. We have a stateful widget for a slider, and all it's doing at the moment is when you drag the slider, it updates itself. And then we have a pie chart class widget here. And all this is doing is it's using a charting package, which we've built in Google, to render a pie chart. And it's taking a little bit of data here. So my job is to work out how to get my slider to interact with my pie chart. So the first thing I'm doing is I'm looking at the disk, and I'm like, OK, well, my chart clearly needs to be stateful, because I'm going to be changing it. So I'm going to go up here, and I am going to use our awesome Visual Studio plugin. And I am going to make this stateful. So that's great. So now, I have state. But how do I make the states interact together? I'm looking for the equivalent of things like find view by ID, so my slider can talk to my chart. But Flutter doesn't have that. So I think the best way to do this, Filip, correctly so, is I'm going to make a variable here called chart state. And that will let me access it for my slider. And then down here, what I can do is-- yeah, I can see that. FILIP HRACEK: I'm OK. I'm OK. MATT SULLIVAN: I can make this chart state, access my state here, and then I'm just going to continue to return my-- FILIP HRACEK: So just good old global variable. MATT SULLIVAN: It's not that global. FILIP HRACEK: It's very global, but OK. MATT SULLIVAN: And I can't spell. So I'm going to save this. It's probably going to break my app, because I'm changing from stateful to stateless. That's OK. I can do a hot restart. And what we have now is-- OK, so this is state, but nothing's happening here. So I'm going to go over to my slider. And this is now going to get very easy, because I can go, oh, I've got this on change. It's setting state for the slider. I can access my chart state, and I can just call set state here. Because this makes sense. And what I can do now is I still have access to my chart. Now, what I haven't done and I forgot to do is now that my state is going to change, this needs to be a var rather than a final. And I can access this. And then all I need to do is use a simple helper function that we have to create data. And look, I can pass on my value, and I can save this. And we'll do this just so that all my state is wired up. And lo and behold, I mean, that's a few lines of code. It's working really nicely. So I think that-- what are you laughing at? FILIP HRACEK: Yes. OK, let's figure out what different things are wrong with this example and with Matt's approach. OK, let's go back to the slide. Thank you for being very instructional. OK, so let's call this Matt approach from now on, in general. MATT SULLIVAN: If I see #MattApproach-- FILIP HRACEK: Yes. So first of all, Matt was strongly coupling widget, so my slider knew a lot of things about my chart. And that is not very scalable if you have a larger app. Secondly, Matt was globally tracking state, which is never a good idea. It's never a good idea, especially for a unitless thing. But in this case, for example, if you had too many charts, then they will somehow be synchronized in terms of what they show, so not a great thing. And then lastly, we are pulling said state from outside the widget. And that is really a bad idea, because, A, good luck figuring out what exactly changed your state from where, and secondly, you could even actually crash your app. So let's not do that. MATT SULLIVAN: So never trust a line of code I ever write is, I think, what you're trying to say. FILIP HRACEK: Yeah. What Matt was doing is he was actively fighting the framework instead of letting it help him. So what do we mean by letting it help him? First, you need to realize what all declarative frameworks have in common-- this one, this thing. UI is a function of state. So in our case, the pixels on the screen or the layout is the state, the current state of your app run through your build methods. Your build methods declare. That's why we call it declarative. They declare this is where the screen should look like at any given moment, given any given state. So that makes it very easy and very predictable, and you have only one code path to every screen. That's what's great about state management in declarative frameworks. You can see UIs on the left-hand side of the equation, which means no UI changes other UI. No find view by ID or anything like that, unless you do something like Matt did. OK, so what does it look like in the widget tree? We need something, some state that will be approachable or accessible to both my slider and my chart. So we do something that we call in any declarative framework lifting state up. So we lift state up, and we have a model that's called my schedule, in our case, and it's just something that we attach to my home page. Because that's the only common ancestor, in our case, to both my slider and my chart. And then when the user taps the screen, my slider sees that tab. And instead of going to my chart directly, it goes through my schedule. It asks for my schedule and tells it, hey, you know what? I was tapped, and this value changed. And then my schedule is responsible for notifying its listener. So it notifies my chart, hey, something changed, and my chart redraws. And then it actually goes down to my slider as well and says, hey, this thing actually changed, and then it changed to my slider. So that's it in a nutshell. But that is theory, and we're talking about pragmatic state management. So what does it look like in code? A lot of people are asking, hey, so what package should we use? So let's give you a little bit of history of state management approaches at Google for Flutter. So one of the first ones was something called scoped model. It is still heavily used. It's very simple. It's basically a direct translation of what I just told you into code. So you have some model at the top, and then you have descendants. And these descendants are notified whenever the model is changed. And it's great and, as I said, it's still used. But it lacks some of the features that you would want from something like that. So we'll talk about the features that we would like to see during the rest of the talk. Another approach that we have is BLoC, which was created for AdWords, which is one of the more complicated apps out there. And it is great. It's based on Rx and streams. But we've heard loud and clear that for many people, this is either too complex for their need, or if they're just coming to Flutter, and they try to find out about a new approach, they don't want to learn Flutter and also learn all of Rx and streams and all of that. So there we were last year. Look at us-- very young and naive. And by the way, good job on having the same shirt this year. [LAUGHTER] [APPLAUSE] MATT SULLIVAN: This is my favorite shirt. FILIP HRACEK: Yeah, I can tell. Yes. MATT SULLIVAN: I do wash it in between conferences. FILIP HRACEK: Good. Good. MATT SULLIVAN: I have a new hat. FILIP HRACEK: Yeah. AUDIENCE: Yeah! MATT SULLIVAN: Thank you. I have a new hat. [APPLAUSE] FILIP HRACEK: So we could say that you managed to change the state of your hat at least? MATT SULLIVAN: Oh. [LAUGHTER] FILIP HRACEK: I'm not sorry. OK, so here we were. And we were thinking, OK, it would be great to have some state management approach that is closer to scoped model but has all these features that we would love to see. And lo and behold, the community came and said, you know what? PackageProvider. PackageProvider is a community-based effort. It was mostly built by [INAUDIBLE].. Thank you. We're using it internally at Google, and it is very close to scoped model. It is very close to what I just told you. It just has some nice bells and whistles that we are going to talk about soon. So in this next segment, Matt will vindicate himself from whatever that was last time he coded. And he will use scoped-- I'm sorry, not scoped, but Provider to build the app again. So if you could switch to Matt's screen again, please. MATT SULLIVAN: Vindicate myself. OK, so what you saw before, why the magical power of Git did not happen-- there we go. And I'm going to get this going again. So we're back to where we were 15 minutes ago with nothing wired up. So the first thing Filip talked about was encapsulating your state and lifting it up. So the first thing we're going to need to do is encapsulate our state. So what I've done here is I have this very simple class called my schedule. And you'll notice that it has a mix-in-- with and dart. Let's just mix in other classes. And this is a change notifier. And what is this magical change notifier? All it does is it adds listening capabilities to my schedule. So now, we get add listener, remove listener, dispose. And hey, I can notify listeners when things change. I have my state in here now, which is still our double. Doesn't have to be a double. It could be a lot of complex state. We only need double here. And I have a getter, and I have a setter. And the setter calls notify listeners so that things know when things have changed. So that's all I have. First thing I need to do is, where am I going to stick my state? FILIP HRACEK: Lift it up. MATT SULLIVAN: So I'm going to lift it up. So I can see here, my chart needs to access it. Slider needs to access it. I could put it in a column, but I'm just going to stick it up here above scaffold. So how do I put this in? Well, I'm going to wrap this in a new widget. And I'm going to use a change notifier provider from the Provider Package, which will provide a change notifier up and down the tree. This takes child, so already have that. And then also, we need to provide it with the actual state object. And I can do that very simply by creating a builder. This builder is going to take context, because they nearly always do. And I'm just going to instantiate an instance of my schedule. So now I've got some state in a tree, which is great. But how do I access this state? So let's take a look at slider. I need to wire slider up to my new state, and there's a couple of ways of doing it. FILIP HRACEK: So there are two ways. You either can use the consumer widget or provider.of. MATT SULLIVAN: OK, so let's use provider.of. I'm going to get access to my schedule by calling provider.of. Then I'm going to tell it what type I'm looking for. And I'm going to give it the context. And so at this point, this is going to go up the tree, going where's the first instance of being provided of my schedule? And it's going to provide me with that. Does what it says in the 10. Then what I can do is instead of tracking my state now through my stateful widget, I can simply do schedule.statemanagementtime. And then to update it, because this is a closure, which gives me a value, I can do schedule.statemanagementtime equals value. I'm going to save that. And let's get rid of this because this is distracting. And you'll notice that my state still works. Let's prove that by doing a hot restart. You'll notice that my state still works, but I'm no longer tracking it in the stateful widget. In fact, I could change my slider now to a stateless widget, because it doesn't actually need to track its state itself. So this would simplify right down to the build method. Now I need to wire it in my pie chart. So I could do provider.of again, but let's do it slightly differently. This time, I'm going to use the consumer widget-- consumer. Again, tell it what you're looking for. And this time, this is going to take a builder, which is going to provide a few things. The builder is going to give us a context. Most importantly, it's going to give us access to our schedule object. It takes an optional child, which we're not going to use, and that's it. So now I'm wired in. And instead of having my hard-coded series list, what I can do now is I can use my helper function to access my schedule.statemanagement. I'm going to save that. And if I've done this correctly-- there we go. So all wired up. Am I thus vindicated? FILIP HRACEK: Yes, I think you are. MATT SULLIVAN: Thank you. There we go. [APPLAUSE] FILIP HRACEK: Back to slides please. So fun fact-- we actually had our own package, kind of like scoped model version 2, and then we released it earlier this year-- open source and everything. And then we realized, Provider is actually much better. So we're using Provider instead. So don't use Provide. Use Provider. MATT SULLIVAN: What we want to point out here is that we don't want to reinvent the wheel every single time. We're much happier working with the community to build these things. And now we're working with the creator of Provider to extend it further. And so why build it ourselves? We're more happy to deprecate our own stuff. FILIP HRACEK: Absolutely. Yes. So pie charts and sliders are cool, but let's talk about a more real-world approach. We have this app, which some of you may have interacted with here at I/O. If you haven't, you can go to the Sandbox and try it out. It is also available on both App Store and Google Play Store. And also, the code is available in open source on this link. And this app was built from the very beginning to be very live. There's a lot of state changes. Everything changes all the time and different pace. And we want to make sure that the UI reacts to these changes always. So the cool news is that most of it is basically what you just saw Matt do. So we have a consumer here of something called company. And then whenever that changes, we can do just that. So that's cool. MATT SULLIVAN: Oh, right. Over to me. We rehearsed this, right? So does this now mean that we can go from our back of the napkin to creating our fully-fledged app just by using Provider, and we're done? FILIP HRACEK: No. No. No. And that's the rest of our talk, talking about the nuances about using something like that, hopefully, to show you the experience that we had with Developer request. First is disposables. So many times, your state wants to clean up after itself. You have a database connection. It needs to be closed. You create a file. It needs to be closed-- all these things. So what do we do there? Fortunately, Provider actually, by default, is prepared for this. So if you use Provider of something like foo, you have a builder, but you also have a dispose callback in which you can dispose of the object that you're providing. So you know that once it's not in the tree, it will be disposed of properly. So that's cool. It's even better if you use something like ChangeNotifierProvider, because we know now that your user here is an actual change notifier. Change notifiers do have their dispose methods. So we can just call it for you instead of you giving us the callback. So that's what you saw Matt do. And he did not even need to think about it. It was just made automatically for him. MATT SULLIVAN: Mm-hmm. So handling state is great, but you don't want to have a giant, big, single state class with tons of complex state in there. Typically, you're going to want to break your state up into different components, different classes, different objects in order to make it a little more fine grained. The good news is that with Provider, you can do just that. So there is a handy widget called MultiProvider. And you'll notice, when I was getting access to my schedule state, I was specifying the type. And you can create multiple providers for different types. And when you specify the type, it will search up the tree to find the appropriate type. So you can compartmentalize your state into different models and be able to access them efficiently throughout the tree. FILIP HRACEK: Sometimes you want to take a more fine-grained approach to listening to your model. For example, in developer request, we have this screen. And this screen shows a list of characters. And the list itself can change, because we can add a character, for example. But also, each of the characters can change. And for performance reasons, we don't want to rebuild the whole thing every time any of the character changes. So what we can do is what I call listening to parts. Here, for the whole screen, we're only listening to character pool. So all we do is whenever a character is added or removed, then we rebuild the whole grid view. That's OK. But then in each of these grid tiles, we're actually listening to that character that's shown. So if this character changes, it won't rebuild the whole screen. It won't rebuild the other characters. How does it look like in code? You realize that change notifiers can have other change notifiers in them. So here, we have character pool, which is a change notifier. It has a list of characters. Those characters are change notifiers. And in your build method, then, you first listen to character pool. That will not change very often, because we're not actually adding or removing too many characters per frame or whatever. And then in each of the titles, you get the character from the pool. And then you provide that character to the rest of the subtree. So now the subtree only rebuilds when that character rebuilds and not the others. MATT SULLIVAN: So at this point, you must think we're completely in love with change notifiers, because that's all we talk about is change notifiers. I really like Provider, but I also love managing my data with streams. And I use a modified version of the BLoC pattern. And I was a little sad. Provider came along and I'm like, oh, change notifiers is nice, but I prefer streams. Well, you know, guess what? There is a stream provider in the Provider Package, where you can give it a stream, and it will update when new data comes through. And it's not just streams, and it's not just change notifiers. You can pretty much provide anything. So for example, here, this is a very simple example, where we're just providing an instance of player details, which, in this case, player details, once you log in, don't change. And so this won't update. There's no notifications to say that it's changed. But we can provide this in our widget tree, and then any of the child widgets can access that. So change notifiers are great, but you have the flexibility to use whatever method you would like or whatever model you would like in your state. FILIP HRACEK: Measurement-- my favorite topic. So performance measurement-- we know about some things that we can always say about performance in Flutter. You try to rebuild as few widgets as possible anytime they change. You don't want to rebuild the whole tree. We also try to not rebuild more often than we really need to. These are all nice, but at some point, you will hit the real world. And then you realize that it's not all black and white. Sometimes there are drawbacks. Sometimes there are premature optimizations. Sometimes you just don't know what works and what doesn't. So what I'm here to say is you can use Flutter driver, which is our automated framework that lets you exercise your app automatically, and you can use Flutter's profiling tools to get the data. And then you can actually have data on which you can base your next move. So what you see here is that's my desk. And that's one of the early versions of Develop Request. And as you can see, it is completely automatically being played. Each version, we are doing this, like, 100 times to make sure that we gather enough data. And what this allows you to do is that then if there is a decision before you, and you're not so sure-- like, for example, here, we have a center, which always notifies listeners. I know that the text is small, but you don't need to parse all of that. It's a very simple setter, not optimized. And then we have a hypothesis that this optimized thing will perform better. We think that, in the setter, if you check whether or not you've actually changed the value, and then don't notify listeners in that case, then it will improve the performance. But you don't know. That's a lot of new code that you need to maintain. There could be bugs in it. Do you really want to do this? Also, you're doing more work in the setter. So maybe that will offset any kind of advantage that you will have from not notifying listeners too often. So you don't know. And you can either argue for hours about this, or you can measure it and see for yourself. So in this case, I'm happy to say, it actually did optimize. So the optimized version, if you measure the CPU time taken by the full exercise, went down by, I think, something like 12%. And it is statistically significant, as you can see from the mustaches there. So that's cool. And now we don't need to argue about this anymore, and we can go on. What I'm trying to say here is not that do this. Many times, we did something similar, and then it did not have an effect, and we just reverted. So you want to have some measurements. You want to be able to have data-driven decisions about it. If you want to know more about this, I just published an article called "Performance Testing in Flutter Apps," and you can use your favorite search engine to search for it. MATT SULLIVAN: So clearly, at this point, you're like, Provider is for me. I'm going to use Provider for everything. All my state is going to be Provider-driven. Well, Provider is great, but there are cases where you just want to keep things super simple. So, for example, on the left here, we have our widget tree. And you can see, we've had to lift our state up. Because we have multiple widgets in the subtree that are dependent on that state in order to render themselves correctly. And over on the right, we have a single widget that is the only widget on which is modified by this state. And this is an artificial example. You'll see examples of this everywhere. So for example, Flutter's animation framework requires that you track the state of the animation-- animation controllers and whatnot. And typically, they're encapsulated into a single widget. So in the case of this, why lift it up, provide it, and access it down when you can simply use a stateful widget to encapsulate the state and to encapsulate your UI in a single place? So a rule of thumb, if you have to lift state up, because multiple widgets need it, Provider is a great way of doing it. But don't use it needlessly when there's not much point when only a single widget requires it. FILIP HRACEK: Lastly, but not least, testing. So you want to test your app, obviously. The cool thing about Flutter is it comes with a headless testing framework. So you can test your widgets in the context of other widgets. And Provider is a widget, so you can do something like this. You can pump change notifier provider, and then test that it works well with the rest of your code, with the rest of your widget tree. So that's one thing, and you should definitely do that. Because that provides you one facet of testing. But it's also good to note that the change notifier or whatever you're providing is just a simple class. Change notifier does depend on Flutter, but only on change notifier class, which is like 100 lines of code with comments. So if you want to have a very contained unit test, then you can do that. Here, we are having our character, which is a change notifier. We just make sure that upgrading the character actually increases its level. That's it, and it works, and it executes in milliseconds and not hundreds of milliseconds. MATT SULLIVAN: So to summarize, hopefully, we're giving you an indication of why it's very, very, very important to choose an appropriate means of managing state inside your app from day one. You want to get that right. You don't want to really mix and match. You want to nail that down so that your app, as it grows over time, will be maintainable and understandable. We've taken a journey through a bunch of different approaches, and we've landed on what we feel is a pragmatic approach. But scoped model is still great. And for apps that maybe are not so sophisticated in terms of its state, it's a perfectly good approach. If you use BLoC, and you love Rx and streams, fantastic choice. You can have very fine-grained control. And there's a bunch of others out there. There's a really good implementation of Redux. There's multiple good implementations of Redux for Flutter. And if you're familiar and comfortable with the Redux pattern, then that is, again, a perfectly good choice. But make a choice and stick with it. And if you're new to Flutter, or you're new to declarative frameworks and how to manage state, Provider is a really solid, good choice to make, which will hold you in good step as your app grows in complexity. So with that, we would like to say thank you very much and enjoy the rest of I/O. [MUSIC PLAYING]
Info
Channel: Flutter
Views: 341,878
Rating: 4.9555631 out of 5
Keywords: type: Conference Talk (Full production);, purpose: Educate, pr_pr: Google I/O
Id: d_m5csmrf7I
Channel Id: undefined
Length: 33min 24sec (2004 seconds)
Published: Thu May 09 2019
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.