Observable Flutter: Diving into Riverpod, with Rémi Rousselet

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
CRAIG LABENZ: Hello, everybody. Welcome to an exciting episode of Observable Flutter. I am really looking forward to this one today. My name is Craig Labenz, and I am your host, as always. Before we get too deep into this, I do want to cover a few ground rules. Remember, everybody, this is the Flutter community and we are-- we take respect for each other very, very, very seriously. Now, especially today, as we are talking about, I think, inarguably, the most controversial, the most exciting topic in all of, well, really, UI development, state management, it's going to be very important that we all remember to stay respectful, especially in the chat. And, today, there's an extra rule. Please do not discuss other state management solutions in the chat. Today, we are talking about Riverpod, and that is because today's guest is none other than Remi Rousselet, whose name, I think I'm saying that right. I haven't said it in a long time. Remi will let me know in a moment. He is a software engineer at Invertase and the creator of-- originally, it was Provider, the first anointed state management solution in Flutter. And then Remy thought that he could do better than choosing everyone's favorite state management solution, and went on to apply his learnings from Provider, and started working on what we now know as Riverpod. And today, we are going to dive into Riverpod. And we'll have a bit of talking about how to use it, but also a bit of talking about how it works behind the scenes. So I'm going to bring Remi in here. Remi, how you doing? REMI ROUSSELET: I'm doing fine. What about you? CRAIG LABENZ: I'm doing great. I am very excited, I have to say. I have not-- I've intentionally not really done any preparation for this because I want to learn along with everyone. And I want to be able to ask questions from an ignorant perspective. So I'm very excited to see what is under the hood of Riverpod. Of course, any of us could have just gone to GitHub and Reddit, ourselves, before the episode. But I wanted to hear it from you, Remi. I didn't want to draw my own conclusions. So I'm very excited. REMI ROUSSELET: Nice. It's also a quite interesting time to learn about Riverpod, considering, if you look into [INAUDIBLE],, some things may be still in progress. So hearing it from the source makes things easier. CRAIG LABENZ: Yeah. Yeah, indeed. It's funny. The phrase, "hearing it from the source," is interesting when applied to open source software because [LAUGHS]-- REMI ROUSSELET: I guess, yeah. CRAIG LABENZ: Yeah, reading it on GitHub would also be hearing it from the source. But better to hear it from you. OK. So I think you have some stuff you might be ready to share. I'm going to click a button here and-- [CLICK] --there you are. REMI ROUSSELET: Ooh. CRAIG LABENZ: So what are we looking at? And what do you want to talk us all through, to kick this off? REMI ROUSSELET: Well, what you see right now is the source code of the counterexample you can find on GitHub, on the Riverpod repository. It's a basic counter that you typically see in your Flutter Create-- default for a Create application, but implemented with Riverpod. So as you can see here, we are importing flutter_riverpod and also for annotation, which we may talk about later, using this ProviderScope widget, which is a flutter_riverpod widget-specific Consumer, those new, fancy things. So yeah, we're not using a stateful widget here to make the incrementation. We're actually using Riverpod only. Yeah. CRAIG LABENZ: Nice. And one thing I know you've talked about during-- in your talk at FlutterVikings, back in September, in Oslo, was that you avoid calling setState to trigger a re-render. And one thing I would love to hear more about is-- what setState does is call markNeedsDirty, markNeedsRebuild, or whatever the method is. Are you still calling that method? How are you telling Flutter to re-render if we're avoiding setState? REMI ROUSSELET: Well, see, what I mean by avoid using setState-- well, I don't truthfully remember what I said at that time, but I doubt that it was about not invoking setState at any point in your application. It's likely more about the way you change your state, so more about the trigger mechanism, rather than using setState internally to update your UI. Because technically, most, if not all packages using-- doing some form of state management use some form of setState or markNeedsBuilds. So the point is more about doing an approach-- using an approach for updating your UI that is the most maintainable possible. And my issue with setState, when we use a stateful widget, is that it's a very interactive way of doing things. So you have your initial state, int counter. And then you sometimes update it in your UI with counter++. And the thing is, when using setStates and all-- CRAIG LABENZ: Of course, pretend setState-- REMI ROUSSELET: --the logic-- yeah. CRAIG LABENZ: --isn't like a button callback. [LAUGHS] REMI ROUSSELET: Yeah, yeah, yeah, of course, of course. It might be a bit broad if we're just looking about this right now. But the logic tend to be a bit all over the place. And you may sometimes forget to invoke it in some places or things like those. And of course, I don't really have a strong statement against setState. It's more about what Riverpod promotes is a slightly different approach where, using Riverpod, things update by themselves. You don't really have to think about doing setState at all. It's just not even really needed anymore. If-- Same thing for [INAUDIBLE] for example, or loading state [? unlink, ?] in many cases, you don't even have to think about it. You don't have to catch exceptions and things like those. So it's more about changing the way you design code to make things better. CRAIG LABENZ: Got it. Yeah, yeah. OK. Yeah, it makes sense. So I think folks who have used Riverpod in any capacity before have read that the first thing they need to do to get Riverpod going-- exactly, is the ProviderScope. So what can you tell us about ProviderScope? Is it just an inherited widget? I've honestly not looked because I want to keep the mystery for myself until right now. REMI ROUSSELET: Well, it's a stateful widget, which, in its build method, returns an inherited widget. The uncontrolled ProviderScope is a class you can use, too, which, this time, is an inherited widget. Oop-- removed too many things. But it doesn't really matter. It's more about-- it's like MaterialApp where if you want to use Theme and all those sort of things in the Material application, you wrap your application in MaterialApp, Material. And that's the same thing-- ProviderScope is basically the same thing, but for Riverpod. So you-- By wrapping your application inside a ProviderScope widget, you're basically enabling Riverpod for the entire application. In fact, even recently, I raised a linked package which contains different warnings for when-- if you use Riverpod to help you with your development. And one of those warnings is, if it detects a main function and there is no ProviderScope, but you're using a Riverpod, it warns you. Hey, you forgot to use that Riverpod as a ProviderScope widget. Because in many cases, sometimes you forget to use it. And so it has a quick fix to add it for you. CRAIG LABENZ: Nice. [LAUGHS] Nice. So, OK, this is a great detour, I think, into how a Riverpod developer would get all of this juicy, extra linting set up. REMI ROUSSELET: Yeah, sure. So basically, when you get started with Riverpod, in your pubspec, of course, you would remove these past dependencies and use version numbers instead. I just don't see a repository, so I'm using past dependencies. But typically, you would install either flutter_riverpod or a hooks_riverpod. Hooks is more advanced. You don't really need to-- if you don't use Flutter or Hooks, you don't really need to consider it. That's more for the advanced users. For now, just stick with flutter_riverpod if you're a beginner. The plain Riverpod package is if you're not using Flutter, just maybe making common line-- CRAIG LABENZ: A Dart package, yeah. REMI ROUSSELET: --or server application. You can use it on the server, too. The Riverpod package is pure Dart code, so you can use it everywhere. So yeah, you install for Riverpod. And maybe I would recommend installing the generator, too. So it comes in two packages, which is the riverpod_annotation package and a riverpod_generator, which is right here, which is in your dev_dependencies. Then, if you do that, you have to install build_runner, too, because you're using [INAUDIBLE],, which you may not need in the soon future, but that's a different topic. And, finally, the last step is I would recommend installing the new riverpod_lint package here, which will provide custom warnings for you. So to do that, you have to install riverpod_lint in the dev_dependencies and also add custom_lint, which is a site package that enables riverpod_lint to work. And once you've done that, you need one extra step, which is create an analysis option dot yaml file. Put this right next to your pubspec.yaml. And you have to specify another plugin, plugins, custom_lint. CRAIG LABENZ: custom-lint, OK. REMI ROUSSELET: And then, from there-- CRAIG LABENZ: custom.lint must already be registering itself with custom_lint. REMI ROUSSELET: Yes, exactly, like with build-runner. You install build_runner, and then you install your generator. Here, you install custom_lint, and then you install your lint package. CRAIG LABENZ: Got it. Great. OK, all right, so we've seen RiverpodScope. It's an inherited widget. How does the RiverpodScope inherited widget-- which is a funny name, uncontrolled RiverpodScope, how does it know about all the different Riverpod providers that people create along the way? REMI ROUSSELET: Well, that's the thing. By default, it doesn't. It knows about them as soon as you start using the providers. Providers are all lazy loaded, and so they are not they are not-- there is no state associated with the providers unless you start using the provider. And so by default, your ProviderScope doesn't even need to know about the providers unless it's used. So yeah, when you first read your provider, the ProviderScope will be interacted with by the different [? provider ?] APIs, and then, at this point, it will start to know about this provider. CRAIG LABENZ: Got it, yeah. Can we look at some of that code? I'd love to see how that comes together, how-- REMI ROUSSELET: Yeah, sure. CRAIG LABENZ: What if we pass Riverpod to Provider? Is there a map that it puts it in? How does it hold on to all these? REMI ROUSSELET: I mean, maybe one first step would be to talk about the consumer widget. Because we haven't talked about how to consume providers, and maybe talked about how to define a provider. So first, defining a provider, since we're using the generic [INAUDIBLE] syntax, is we're making a Riverpod annotation, and then we annotate [INAUDIBLE] our class [INAUDIBLE] function. So if I want to do a counter, I can do count, and that's just part of your account graph, which is the function name, is a capital-- and then followed by ref. We name the variable ref, and then we'll return whatever, so 0. [INAUDIBLE] runs. This is, no, fixed. And inside our UI, we need what we call a consumer widget to consume providers. So we don't rely on stateless widget. We use a custom widget type. So I can use a Riverpod [? link ?] to convert the stateless widget into a consumer widget for me. I do a quick fix, convert to ConsumerWidget, and here we see it changed the base type, and it added one extra parameter to the build function, which is a WidgetRef. This ref object is what allows us to interact with providers who can find it here on widgets, and in providers, too, because providers can interact with those providers. And so here, using this ref objct, we have a few methods, as you can see [INAUDIBLE] the completion, like the ref.watch function, which gets a provider as a parameter. And it will read this provider, and it will return the site of that provider. [INAUDIBLE] and so here, I'm using ref.watch, so it will read the count provider, which is this one, and listen to the state if somehow it changes, which it may. And if it does change, then the widget associated with it will re-render. So that's the basic logic for using Riverpod. And so to answer your question about how these are interconnected, we can look at ref.watch function here. So let's see, that's just an interface. If we look at the implementation, we'll see here, so that's the implementation of the watch function basically taking the provider as parameter, and then yeah, container. Container is-- it's a provider container object. The provider container object is an object you would typically interact with if you're just using Riverpod in a pure dart In a Flutter application, you typically use provider scopes. That physically is the same thing under a different name because of a different platform. A bit confusing, I know, but long story short, provider scope and provider container are basically the same thing, just one is a widget, and also when it's just a plain dart class. ProviderScope is an inherited widget, which expose the provider container, as you can see here, with the provider scope widget, which has a ProviderScope.containerOf [INAUDIBLE] context. Returns a provider container using the inherited widget API. So here, I have a provider-- I have a container. You can see, type-- ProviderContainer-- and sends this provider container object as [INAUDIBLE] listen, which takes a provider and then invokes a callback when the provider changes. And then at that point, I can do something like setState [INAUDIBLE] if I need to. Was that maybe too much? A lot of different things, a bit advanced. CRAIG LABENZ: No, yeah. I think we'll probably continue to hit some of these as we go. Can you return to the main file in the sample? The countProvider variable-- I might have missed something. I was typing something in the chat about your light mode versus dark mode preferences. That countProvider variable that you're passing to watch, where did that come from? REMI ROUSSELET: Yes. Since I'm using the count generator, basically when you define this, it will generate a variable associated with this. You don't pass count directly. You pass count and suffix it by Provider. CRAIG LABENZ: And so that's in main.g. REMI ROUSSELET: So here the function is named count, so that's countProvider. If I rename it to countfoo and I save, then that's countfoo here now. CRAIG LABENZ: Probably lower case-- REMI ROUSSELET: Of course, I renamed it here, but I forgot to rename the parameter, which is based on the name, too. There's a link for it to quick fix it for you. CRAIG LABENZ: Nice, nice. REMI ROUSSELET: So countfoo, and lose an underscore here. CRAIG LABENZ: OK. Yeah, nice. OK. OK. So it might be nice if we saw a very generic-- maybe we just look at the generated code. But I'm wondering-- like that generated code there, we have the Riverpod annotation. What is that equivalent to? What it generates-- what if we just collapse into this? REMI ROUSSELET: Honestly, I would prefer not talking about what the transit code is. I mean, we can talk about that, the fact that it's a provider. But from my experience, beginners with Riverpod tend to be confused by the old syntax. They tend to see a lot of different types of providers and end up asking the question, which provider should I use, which in my opinion, that's the wrong question to ask, which is why the new syntax is here. It's supposed to remove the question of which provider do you use. Instead, you just focus on your logic. The idea with the syntax is that's just a blind function. You can do anything you want with a function. You can make it async if you want to. You can return a string if you want to. [INTERPOSING VOICES] CRAIG LABENZ: It doesn't seem like this would-- how would I increment this if I wanted to rebuild the counter using one of these annotated providers? Because it's just giving me 0. How do I do something with it? REMI ROUSSELET: Yeah. Well, first, if you were to return a string, for example, you could maybe update the value over time if you wanted to. Like every two seconds, you obtain a new one. Well, GitHub Copilot is having a nice day. [LAUGHTER] You could technically do that if you wanted to. [LAUGHS] Or-- CRAIG LABENZ: Nice. REMI ROUSSELET: If you want to have methods to update this thing, like doing an increment method, you can convert your function into a class. Let me go back to the previous syntax here. So again, we'll rename this. Yeah. Here we just have a blank function. And we could convert this into a class using another quick fix from the Riverpod [INAUDIBLE] package, which now makes a class. So that's the class name, which is the function we had before, count, now is in uppercase, which has to extend underscore dollar. And this thing will disappear once we have metaprogramming, but that's for the future. And so here, we see a function named build, which is basically the function we have here but in a class version because classes don't-- basically, if I do more fancy stuff here, Hello world, and I do the converter again, you see, that's-- once again, we see our Hello world come up again. This function is strictly the same thing as this function. Basically, the both codes are strictly identical. It's just using this function syntax is slightly simpler if you don't need all this. And so anyway, I'm going on a tangent. CRAIG LABENZ: No, I think tangents is the point of this. This is perfect. REMI ROUSSELET: Yeah. So now if you have a class syntax, you can now define methods in here. As you can see, it autocompletes. Yeah. So then we make an increment method. And so in here, I have a state property and various other properties. I have the state object, the ref object, which we saw previously-- same thing as this one and the one on the function. So I can just go state++ and that's all. And so in my UI, now what I can do is I can make a button, ElevatedButton, onPressed here. And I can do-- make sure once again it's working fine. And use this ref object, which we covered previously. I can obtain the provider. And since we defined a class, we can do my provider .notifier. This notifier, what this does is it will return you the instance of this class and so you have access to all the methods defined in here. So I can just invoke increment. You see if I go under definition of increment, it just leads me here. Nothing fancy in here, nothing mysterious under the hood. CRAIG LABENZ: So countProvider-- one thing I want to-- I wondered-- and again, I could have just looked this up, but maybe part of me knew that I was going to one day start a live stream and invite you on it. If on line-- actually, the line numbers are hidden. But if you scroll down a little bit, the line you were just writing, the ref.read in the ElevatedButton-- so read, when you pass it-- let's say you just passed it countProvider, not the .notifier, as we Americans call it, this returns just that state object, which was an integer, right? REMI ROUSSELET: Yes. CRAIG LABENZ: So how are you attaching .notifier to integer to return the surrounding class? How are you doing that? REMI ROUSSELET: I'm not, but that's the point. Basically, the provider, this variable here, the generated variable from this definition, is an object-- CRAIG LABENZ: Oh, it's not an integer. It's a way bigger thing. REMI ROUSSELET: Well, it's not an integer at all. CRAIG LABENZ: Right. OK. REMI ROUSSELET: Like, this countProvider is an object which enables you to obtain either the integer or whatever else. This variable is just a means of communication. It's not a state in itself. In fact, this could very much be a constant. There is no mutation involved. It's pure immutable object. CRAIG LABENZ: I see. OK, so what's the syntax to get the actual integer off of countProvider? I forget. REMI ROUSSELET: Just passing the provider by default, no .notifier. CRAIG LABENZ: OK. So there is some magic here. [LAUGHS] So countProvider, even though it's an auto-- REMI ROUSSELET: I can write a basic example without Riverpod if you want. It's basically you have-- let's call it provider. It's a generator object. I'll make an instance here, final count myProvider Provider, which is of type int. That's just this. And so now I have my read function, which is void read, which takes a provider of type whatever. Pull out the Zen Mode. Yep. CRAIG LABENZ: So does provider int there need to also be Provider T in your read method signature? REMI ROUSSELET: Yes. Yes, yes. So I need another thing here, an abstract class, ProviderBase T, which just implements ProviderBase. OK. And then I use ProviderBase here-- class. OK. And so in here-- so here, if I do main, I can do read myProvider, which is here, which is our user-defined provider of type int. And then I'd get an int here-- type int here or type-- CRAIG LABENZ: Yeah. So what does the function body of this read method kind of vaguely look like? REMI ROUSSELET: It depends on where you're invoking it. If you're in a consumer widget, it uses the context API-- it uses the BuildContext API to do ProviderScope.co ntainerOf(context). You obtain the container from it. And then it returns container.read the provider. [? Obviously, ?] I made a new class so it doesn't work, but that's basically the implementation of it. I'm pretty sure I can look into it right now, and it would be exactly this. Read there-- no, not this one. Read this here. The ProviderScope.containerOf, and just return read just in one line, because why not? But we see base class, ProviderListenable. That's the ProviderBase I just made here-- same thing. And so for the .notifier, it's basically in the provider object. I ask basically, ProviderBase-- let's pretend, I don't know, String here, get notifier. And so here, I can do .notifier here. And so now instead of an int, I return-- wall, ProviderBase-- I forgot the D, yeah. Now I get the string from-- if I remove it, I get an int. CRAIG LABENZ: I see. REMI ROUSSELET: Are you following? Think of this variable here as just a key. In fact, in one of my early experiments when looking for a way of improving over providers, when I used the good old [INAUDIBLE] widget fashion, one of the common problem with provider was I have two providers of the same generic type. How do I pick between both? And so when looking up for alternatives, one of the solution was, say I write some provider code, MultiProvider. Yeah, MultiProvider. OK, yeah. And maybe make a second one. Yeah. So I add both. Both [? do ?] an int. And I want to use it-- I want to be able to [? rip ?] out. One of the things I was experimenting with was finding a consumer-- oh, god. Went to Zen mode again-- builder context here. And I do context.watch, pass the int type, I want to be able to decide, do I want this one or this one? And with just returning type, we were not able to do that. And so I was experimenting with maybe having an optional key-- well, not a key because it's a keyboard in Flutter. But I don't know. You need a-- CRAIG LABENZ: Something, yeah. REMI ROUSSELET: id-- something like that-- id string. And maybe in here, id, would specify which one you want, which would work. But that's a string. And you can do a typo in here and you'll get an exception. CRAIG LABENZ: Runtime errors. REMI ROUSSELET: And I didn't quite like that. And at some point, I realized, hey, wait a second. First of all, that string should be a variable here called foo. And I should reuse it. That would be the first good practice you would do for this one. But then at some point, why just is a string? Why not use an object in here, which would also add the implementation of this function here? So now, with just this foo object, I don't need this thing here, and I can just do context.watch this. And so I no longer need to insert the provider [? inset ?] with a tree. And also, it has an added benefit of there is never a case where we are not able to find the provider because there is always a default implementation, which is this one. So we don't have the provider not found exception. We can just default to do that. So it just removes the provider not found exception. And so it solves kind of both problems at the same time, which is a neat improvement. And so one of the later iterations of the syntax was now-- before, we used to do this-- Provider int and with the ref and return 0. And now, with the new count generator, we do it with that function. Basically, it's this. It's identical to this. It's just this is kind of weird. Like, many people see a variable here and they're like, am I defining a global state? And I've always heard global variables are bad. Why is this not bad? I'm confused-- which is a valid concern. But if you followed, this foo variable is actually just a plain object with nothing in it. There is no foo.state in here. You cannot do that. There's no such thing. It's just a key to just-- like I mentioned before, it's just a plain string. If I wanted to-- and I mentioned in the experiments before, it's just a combination of a unit key with a default initialization function. So whereas, if we use the generator syntax where, again, both of these are equivalent, we now see a function instead of a variable. And so it feels more natural to users because obviously, global functions-- or if you do a class, we'll see it overrides-- if you define a class, nobody see, oh, no, I defined a class but it's a global class, and global classes are bad; or, no, I defined a function but it's a global function. So there is no such concern. It's a lot more natural. It also has added benefits of suddenly a lot easier to pass parameters to providers, because we have the full power of a function. You can just do required int id here if I wanted to. And so if I let-- fix the errors [INAUDIBLE] because I messed it up everywhere, OK, and provider-- and did I-- oh, yeah. Yeah, yeah. Because here, I defined-- we still have our countProvider, but now it has-- it requests an id as parameter. And so when we try to read the provider, we need to specify the parameter of course. And so now, I have to-- kind of a function, you invoke it, and you pass your parameter. CRAIG LABENZ: And so now it knows that it's-- [INTERPOSING VOICES] REMI ROUSSELET: it's an integer, but yeah. I could even use optional parameters if I wanted to, or maybe even default values. Also, putting 42 is just fine. You get a lot more flexibility on how you pass things around. The syntax is much nicer. CRAIG LABENZ: Now, when you added the other parameter, countProvider turned into a callable that you had to invoke. If the parameter is optional, do you still have to invoke it? REMI ROUSSELET: Yes. CRAIG LABENZ: OK. REMI ROUSSELET: We could make it optional, but I mean, I don't see much value in it. Just to be consistent, as soon you pass parameters, just make it a function. CRAIG LABENZ: You have to call it. OK. REMI ROUSSELET: Yeah. CRAIG LABENZ: OK. What error do you get there, by the way? It's just going to be like a parse error, right? Just like a-- REMI ROUSSELET: Yeah, its argument perhaps not-- it's not-- it's expecting a provider, but it's not a provider. It's basically a function. It's a class with a call method, which is basically a function. The slight difference is that, as opposed to just a normal function-- because technically, we could do Provider The countProvider and return that. We could do that for this thing. But the slight issue is that we cannot have methods on functions if-- and one of the interesting features Riverpod has is it offers some testing utilities for your providers so you can mock them if you want to. So if I want to show you how to mock a provider, basically you go to your provider scope, or provider container if you're just in plain Dart. And there are optional parameters in here. You can specify overrides. And here, you can pass providers you want to override. So here, the [INAUDIBLE] is correct once again. And well, of course, this removes the parameter for now. I want to override the default implementation of my counter, which is just returning a custom value. And so now if I read the countProvider, it will return 42 instead of invoking this function, which is useful for testing, because maybe you have a service here and it's doing HTTP request, and you can instead return a fake service here. CRAIG LABENZ: Yeah, yeah, yeah. REMI ROUSSELET: But it's mock HTTP requests and things like those. And so yeah, that's the reason why it's not using a function because then in using the-- if you pass parameters here, your countProvider still has those override things. Let's see. Should be up to date now, override. Where is it? I think the generator might be confused. Let's see here. Oh yeah, I know. I think I forgot that the count provider currently doesn't generate [INAUDIBLE] but should be fixed soon. CRAIG LABENZ: OK, yeah. When you first REMI ROUSSELET: Let's sync-- CRAIG LABENZ: When you first wrote overrideWithValue, my question was, is this a method that we have to implement or does Riverpod provide it? REMI ROUSSELET: No, no, no, no. It's implemented by Riverpod. You don't have to do anything. CRAIG LABENZ: OK. Oh, OK. Man, we have hit a lot. So can we go back-- REMI ROUSSELET: Yeah. CRAIG LABENZ: --to-- yeah, I want to summarize everything right now, or at least take a stab at it. I loved, by the way, the walkthrough of the mental steps that you took to go from provider to Riverpod. How the problem of not being able to differentiate between two providers of the same type put you on the path of, well, OK, I could give a key, but then why would the key be some arbitrary value like a string? It could be the object. [? Weight ?] objects can be rich. They could just have the other functionality. And things just kind of kept implying-- one step implied the next step, which, at the risk of minimizing all the hard thinking that you did, because you were the only one to figure it out-- the stroke of, or the indicator, I think, of really good design is it seems so obvious in hindsight. Put you on that path of evolving your thinking and your API design from provider toward Riverpod. That was really kind of neat. But OK, so we're-- are we back at basically the beginning now, the state of this file close to when we started talking? REMI ROUSSELET: I can go back a few more. Yes, should be good now. Yes. So yeah, I know that I went on quite a few tangents, so it might be a bit confusing to follow. CRAIG LABENZ: Well, the idea-- REMI ROUSSELET: But-- CRAIG LABENZ: Like I said, I think the tangents are the point. But yeah, I'd love to try to summarize this. REMI ROUSSELET: Yeah. If-- CRAIG LABENZ: And-- REMI ROUSSELET: Maybe if we want to maybe synthesize the benefits of using Riverpod, I think there is a nice example for it. CRAIG LABENZ: OK, sure, great. REMI ROUSSELET: Which is, in my opinion, one of the neat use cases is making maybe a search as you type or an individually loading list where you do a network request and it fetches as you scroll, things like those. CRAIG LABENZ: Yeah, perfect. REMI ROUSSELET: And it's fairly simple to do with Riverpod. Like say, I want to do an infinitely loading list or a sidebar. We can do both, actually. CRAIG LABENZ: This is great because infinitely loading lists are famous-- REMI ROUSSELET: Oh, it's complex, right? CRAIG LABENZ: --edge cases for a lot of state management libraries. REMI ROUSSELET: Yeah, but that's the issue. It's edge cases for many-- it's usually an edge case, but when you think about it, it's very fundamental to what you do with [? modern ?] mobile applications. You often do a lot of network requests, a lot of scrolling lists and all. So you want to make it as simple as possible for-- so yeah, let's do a simple-- let's fetch a counter delayed by two second. So-- CRAIG LABENZ: An oldie but goodie. REMI ROUSSELET: Exactly. That's your ref. There's an async. And we can do await Future.delayed(duration) to three seconds, because why not. Hit return. CRAIG LABENZ: Terrible thing. REMI ROUSSELET: Terrible thing? Oh yeah. CRAIG LABENZ: Three second load. REMI ROUSSELET: Very slow connection. [LAUGHTER] CRAIG LABENZ: You can bounce around three satellites here. REMI ROUSSELET: And so obviously, it's a paginated API, so we want to-- we have different pages. So we want to take a page index, so int page. And so let's maybe show-- return a string with the page index. Well, a list of item. List with the string. CRAIG LABENZ: Yeah, perfect. REMI ROUSSELET: And we'll return 50 items because our page is paginated using a page size of 50. So let's do List.generate(50) and return Hello page, and then the index. Should give up-- CRAIG LABENZ: Great. REMI ROUSSELET: --basic API implementation here. Typically, you would do your network request. With just this, you should be able to do everything. OK. So from here, now let's focus on the UI. Let's collapse him. Also, this, let's remove the counter. OK. Remove the floating button. We don't care about this one, too. This one is there. OK. Let's make a ListView because we always want ListViews. CRAIG LABENZ: Yep. REMI ROUSSELET: ListView.builder, context, index. I don't see anything yet. All right. All right, so I should see a ListView here. Oh, I need to return something. CRAIG LABENZ: Hit return, yeah. REMI ROUSSELET: Return Text hello. Yeah, lots of hellos. Nice. CRAIG LABENZ: Yeah. [LAUGHS] Gorgeous. [LAUGHTER] REMI ROUSSELET: I can't scroll-- oh, wait. Can I scroll now? CRAIG LABENZ: Yeah, why can't you scroll? Something's funky here. Well, put something differentiating. Put the index in the text and then we'll know. REMI ROUSSELET: Yeah, but I mean, we should see some form of condition now. CRAIG LABENZ: Yeah. Yeah. That's-- no, no. No, it's not scrolling at all. REMI ROUSSELET: Yeah, it's not scrolling. CRAIG LABENZ: ListView.builder, body, item. Oh, itemCount? Do you have to provide the count? REMI ROUSSELET: Shouldn't be needed. We can pass it through it if you want, but it shouldn't be needed. CRAIG LABENZ: OK. It isn't fixing it. REMI ROUSSELET: Because that's the point. Like, CIDs, widget [INAUDIBLE] not specified, but we'll see later. [LAUGHS] CRAIG LABENZ: What's going on? REMI ROUSSELET: I can restart on the web. Let's see. CRAIG LABENZ: Really puzzled. REMI ROUSSELET: Might be-- maybe it will work for this project. We'll see if it works. Otherwise, we can do the search bar, too, which is basically the same thing. CRAIG LABENZ: So-- REMI ROUSSELET: Yeah, we're-- while it's loading-- CRAIG LABENZ: Chat seems to think we're doing something wrong. Oh, oops. Chat's coming in, so I clicked on the wrong thing. I didn't even read this. REMI ROUSSELET: I tried both [INAUDIBLE] and-- wait, where is it? CRAIG LABENZ: Tobias says, we can't scroll like this. What are we forgetting, Tobias? REMI ROUSSELET: But I use the trackpad, too, and I believe the trackpad works. Yeah, on web it's fine. OK, let's do web. All right. So we have our earliest-- wow, it's 200 items. Nice. So let's remove the itemCount and let's do something more interesting. So now we want to fetch the items. And so we have a paginated API, so of course, we want to fetch the current item [? in ?] the current index. But here, we're expecting a page. The thing is, we can calculate which page is associated with a given index using some simple math. So for example, on the page, at a given index would be index divided by the page size, so 50. And so the itemIndex within that page would be itemIndex equal modulo the page size. And so from here, I can obtain the page value. pageValue equal ref.watch my fetchItem, and I [INAUDIBLE] page. Provider. Here, page page. Yes, that's [INAUDIBLE] because we want an end. But here, now that I have the pageIndex and the itemIndex, I can just read my provider. So ref.watch by the provider and I specify the page that we defined here, the [? main ?] parameter. And it gives me an AsyncValue of list string. That AsyncValue object is because here, we return this string-- a Future. And so we need to handle loading and error states because a Future can fail and it takes time. So we don't just get the value here. So we need to handle those. So there is-- using this AsyncValue object, we can do something called [? pattern ?] matching using a [INAUDIBLE] on that object. So pageValue and we use something called when. CRAIG LABENZ: Nice. REMI ROUSSELET: It's basically a switch case which forces you to specify all the different cases. So loading, daytime, error. So if there is an error, I can return a text with the error here, and the error takes two parameter, which is a StackTrace, [? too. ?] CRAIG LABENZ: Oh, nice. REMI ROUSSELET: If there is a loading state, we don't have any parameter. It's just loading, and we can return loading. And if we have the data here, we have the list of items. So that's the items. We see list string this time, no fancy thing anymore. And so now I can return Text items and use this itemIndex here. itemIndex here. All right. OK? So there are a few extra things we need to do, but we'll see later. So yeah, it waited for a few seconds, and then it loaded. So as you can see here, we now have already an infinitely loading list. It fetched the first page and since we have a big string, it started fetching the second one, too. And if I scroll a bit, it should start fetching this third one. And as you scroll it, it keeps fetching. CRAIG LABENZ: Nice. OK, and you're scrolling up. This answers the question I was going to ask, or at least suggests how it would behave. REMI ROUSSELET: Yeah. There-- yeah. Let's continue. So first, there are a few extra things you might have to handle, though. I didn't handle everything yet. You still need to handle the case where you reach the end of the list. So for example, typically, an API, if you go too far, if you fetch a page that doesn't exist yet, it will return an empty list, usually. And so we can mimic this here by saying, hey, if a page is greater than 2, let's return maybe just two items. Because maybe we have [INAUDIBLE] two items. a and, I don't know, b. One letter. So that's the last page and it contains only two items. So now currently, if I reach the end, will likely have an error because I didn't handle the end of the list. To handle this, you typically just would do-- if the list of items is-- items.length-- is smaller-- if the item index is greater than the list of items, then you just return null in builder. By returning null in itemBuilder, what this stands for is we have reached the end of the list and we just stopped bringing items. So now if I do the same thing again, we see a, b, I need to just stop scrolling. I'm not-- you see I'm trying? It's just not doing anything. CRAIG LABENZ: Nice. REMI ROUSSELET: Another thing you might want to do is maybe you don't want to show 10,000 loading at the same time, you just want to show only one. CRAIG LABENZ: One for the page, yeah. REMI ROUSSELET: Exactly. And so this can be done fairly easily by saying if using the same trick, if the itemIndex within the page is different than 0, it just return null once again. The trick-- it's a trick you can use again. Now if I try again, you see you only see one loading. And then it fetches. Once the signal continues, it fetches, and if we scroll, it fetches. And now we see the end. So of course, you can use the same trick for the error links, too. And so the interesting thing with this approach is all the business logic we did for this infinitely loading list, it's just this. We have five lines of code, maybe more, depending on if you want to split-- CRAIG LABENZ: Different functions in there. REMI ROUSSELET: --from the spaces-- but yeah. [INAUDIBLE] comes in all, but you get the point. We did [INAUDIBLE] like if somehow the network request fails, the UI will automatically show the error. It's mostly about what you want to do in your UI. If you focus on the UI, you don't focus on the business logic anymore, you can just do your application. CRAIG LABENZ: Interesting. Yeah, I have not done tricks like what you're doing online, 52 or 56, really ever before. That is-- this is a new kind of way to think about translating between my business logic and my UI. And so I'm guessing you've done a lot of this, and you find it handles increasing complexity nicely? REMI ROUSSELET: Yes. Because one of the interesting things, like I said, is it completely removed the need for dealing with-- if there is an error or things like those, because if you were to use a typical approach, like say you have [INAUDIBLE] class Foo, you would typically have the list of all items-- allItems-- in here. And you would have another method in here, a fetchMore. And then you need to handle-- you need to do a try catch. You need to have maybe an error object in here and all, isLoading. And there are so many different combinations of possible scenarios here. I can get very complex. Like maybe the user scrolls very fast and so you're fetching multiple pages at once. Did you handle this properly? Do you have [INAUDIBLE] condition, maybe, if that's possible? Or maybe-- whereas here, it just works naturally. Only what's visible currently will be fetched, and you don't need to deal with this scroll set anymore. You just need to make a scroll controller. You [INAUDIBLE] scroll controller and listen through the scroll set, it's just in the builder, [? all ?] [? you would ?] typically do, you just read your thing and it just does it automatically. CRAIG LABENZ: Mm-hmm. Now, how would I change this if I wanted to have some local caching? So first of all-- REMI ROUSSELET: It's already local cached, actually. CRAIG LABENZ: Oh, just by the widgets? What if I wanted to leave the page and come back? Then, I mean, then you just kind of wire a data layer into this method, I'm guessing, essentially. REMI ROUSSELET: No, no, it is actually already local cached. Like, if I refresh, like if I have-- well, I'm already using here. Consumer-- it's making a-- well, maybe let's make-- don't confuse people-- let's make it StatefulWidget instead. Here. I have a name count called 0. Maybe show counts here. Here now we should see, once it appears, the count. Yeah. And I will add a floating button to increment it, floating button. And it's right here. Of course, it [INAUDIBLE] web. So if I click on it, you see it doesn't re-fetch this thing-- it's already cached. CRAIG LABENZ: Hmm. REMI ROUSSELET: Does it-- I know what you're asking. This question is, you're maybe wondering why when I scrolled up, it started loading again? It's because the page was not used anymore. And so by default-- [INTERPOSING VOICES] Yeah, it considered that since the page is not used anymore, we can safely destroy this state. You can customize this thing by maybe caching it for a longer period of time. You could cache it for five minutes if you wanted to. And if you were to scroll up within those five minutes, it would work. CRAIG LABENZ: And so this is-- it's cached just by the widgets, right? It's like cached [? in the widget in elementary. ?] REMI ROUSSELET: No, by the provider. CRAIG LABENZ: Oh, the provider is caching it? REMI ROUSSELET: Yeah. It's basically your provider container. So when you use you use-- when you specify this provider scope, internally you see that the stateful widget, which creates here-- and the state, where is it? ProviderScopeState. And so the ProviderScopeState has a property which is a container, ProviderContainer, and that ProviderContainer object is what stores all the state's [INAUDIBLE] providers. Is there somewhere in here a map? MapProvider, which takes a provider as key and it contains basically state object. CRAIG LABENZ: And it-- REMI ROUSSELET: Not sure if would here, but-- CRAIG LABENZ: --does it continue to key off the different parameters? Right, because we have a parameter to this method for each page. REMI ROUSSELET: Yeah, it's this thing. You have a parameter for the function, but not for the provider. Because this thing is-- if I go-- if I re-explain what this does, the parameter here, this fetch-- no, this one. Because this function here, it's basically doing Provider fetchItems required int page. It's returning a provider here. And is this provider in spotting-- in this specific scenario, we have a parameter, so it's passing the page. And this provider implementation overrides equal equal [INAUDIBLE] the int page. And we have override operator. That's something like that. And so now if I do-- if I have a map somewhere, not equal provider. CRAIG LABENZ: I see. REMI ROUSSELET: I don't know, whatever dynamic in here. CRAIG LABENZ: I see. REMI ROUSSELET: And I can do that. I can do fetchItems. Let's see. Items, this one. And I can do map here. So since this overrides equal equal, the key would be consistent. So if you change the parameter with a different value, the equal would be different and you would obtain a different state. Which is why, when you see here, you define only one provider, only fetchItem, yet you still see multiple pages fetches at once. CRAIG LABENZ: OK, that is fascinating. I followed that. I'd like to summarize it for anyone watching that didn't and/or check my understanding because maybe I followed some of it incorrectly. But there's this internal provider class that is used as a key in a map that caches all the things. And-- REMI ROUSSELET: It's not even internal. Like, when-- if you go to the count generator and use the old syntax, like [INAUDIBLE] FutureProvider and whatever, the map key is your provider. CRAIG LABENZ: OK. REMI ROUSSELET: So that's what I explained before earlier, when I mentioned that with provider, like the actual provider package, and I mentioned of the id equal "foo" strings, that variable would be your map key. It would be internally inside, I don't know, [? multi ?] provider, if you wanted to do that. CRAIG LABENZ: Yeah. REMI ROUSSELET: That would be your key. But this key here is actually this object here. CRAIG LABENZ: Right, right, right, right, right, right. REMI ROUSSELET: If you go back to the previous discussion at the [INAUDIBLE]. CRAIG LABENZ: And so when you use a method-- well, there was some translation-- oh no, it's because the code generation expands the method into the provider. REMI ROUSSELET: Yeah. It's using a dark feature which is named callable classes. Basically, if you have a class which defines a function named call, and then here final foo equal foo, and then foo behaves like a function. You can invoke it. You don't have to do dot call. CRAIG LABENZ: Yeah. Yeah. REMI ROUSSELET: [INAUDIBLE] for it. But it has the benefit of being able to define methods on it, so I can do foo and foo.bar at the same time. CRAIG LABENZ: Mm-hmm, mm-hmm. OK, so in any universe with Riverpod, whether or not someone's using the generator, their providers are keys in this internal cache. And by overriding equality, which is how items are-- determines how items behave as hashes in a hash map, as keys in a hash map, then old data can be stored. Now, the one hop I haven't quite made is how there becomes-- so remember when I said there's some internal class, and you said no, it's just the provider. But when I have just a method and I call it with some parameter, that-- oh, it's instantiating a whole other instance of that object, is that right? So when we have the FetchItem class, it's internally with code generation, that's expanded into a class. And every time I call FetchItem, it's building an instance of that wrapping invisible class, unless we open the generated file. And then it's that new instance that we've just minted which was associated with a given set of parameters that will both do the work for us-- it will call the method and actually load the data-- and it will create a unique key, which is that instance of the wrapping class, and that sits wherever you've hoisted it to allow for basically memoization. REMI ROUSSELET: Yep. CRAIG LABENZ: Does that-- REMI ROUSSELET: Pretty much. CRAIG LABENZ: --sound right? REMI ROUSSELET: Yep, sounds right. As you can see here, I could have just opened this source code. So we see here that's the variable with-- [INAUDIBLE] the callable class, with the call function, which takes the parameters of our provider, which return the provider. And so that's a custom provider class which takes parameters, and then it overrides the [INAUDIBLE].. CRAIG LABENZ: Yeah, and the hash code, yeah. REMI ROUSSELET: It does a few extra things, too, because there is actually a fancy feature I didn't mention yet, which is if you use the code generator, you actually have hot reload support. Which is basically-- well, I'm on web, so obviously, it won't work. But if somehow during live-- like here, maybe you have multiple pages in your application and you go on a specific page which uses this FetchItem, and you change the source code of this function, in most approaches, nothing would happen because it would still use the previous state. In implementation, it wouldn't reinvoke the function. Whereas even if you don't use the count generator, it would still keep the previous state. Whereas with the code generator, if the code generator allows Riverpod to detect that the source code of your provider has changed. And so once the source code of a provider changes, on hot reload, Riverpod will re-execute this provider. And so if you change this provider, then the network request will be performant again. So you can stay on your page, iterate over your network request, and it will keep updating. CRAIG LABENZ: Got it. OK. OK, I had another question. And then we should look at some questions in the chat as well. I'm sure we're getting a ton of good ones. I'm going to try to remember this one that I just had in my head. Ah, yes. You mentioned that we could override how long Riverpod caches things. How do we do that? REMI ROUSSELET: First, one thing to note is that there are some improvements in progress for this. So for now, we have access to an easy low level API, so it might sound a bit scary. Don't worry, it will change soon. At some point, we had something, but I stripped it-- I removed it because it didn't satisfy all of my concerns about the API. But anyway, in a provider, once again, we can use this ref object and we can call something called keepAlive. keepAlive basically tells Riverpod to not destroy the state of the provider until the return object by this function, which is a keepAliveLink-- until we call keepAliveLink.cancel on this object. CRAIG LABENZ: OK. REMI ROUSSELET: And so I don't remember which one it is-- close. Yeah, close. CRAIG LABENZ: Mm, OK. REMI ROUSSELET: And so the basic idea is when your provider starts, you invoke keepAlive, and then you could do if you want to keep the state of your provider alive for, like, five minutes, you could have a timer. You specify a duration of five minutes. And here, you call the close. CRAIG LABENZ: Yeah. Yeah, yeah. REMI ROUSSELET: In the timer. And so this will keep this state alive for five minutes. But obviously, if you want to do that in a bunch of places, it sounds a bit redundant. You don't want to copy paste this over and over. And the thing is, this API can be actually fairly easily refactored by just making a function. You could do cacheFor(Duration), an extension on ref. Duration. We [INAUDIBLE] we copy/paste this here. keepAlive because we want ref. CRAIG LABENZ: Well, ref is-- wait, is ref the class name? I thought it was like widget ref and stuff like that. REMI ROUSSELET: That's the best class for this object. CRAIG LABENZ: Oh, OK, OK. REMI ROUSSELET: [INAUDIBLE] this here. Oh, that's for AutoDispose. Don't mind me. Technically, you shouldn't have to do this yourself, anyway. Like I mentioned, in the future, this should be something built into Riverpod. You just have to do this for now. So you make-- there is this function and so now you can just invoke this in your provider, ref.cacheFor. CRAIG LABENZ: Oh, that makes sense. Yeah. REMI ROUSSELET: 5. And you can do that in a batch of providers. It's just a one-liner, so at this point, it's very reusable. CRAIG LABENZ: Yeah. Can't get shorter than that. OK, so there is, in fact, a lot of good questions in the chat. And one of them goes back nearly an hour. And Afaq was asking a question similar to what I was asking and you said, wait, I want to hit this other thing first, and we might now be ready. They're wondering, does the watch method rebuild the widget? And that is certainly a yes in the future. It sets up the eventual rebuilding of the widget. But could be time to maybe look at how the watch method works. REMI ROUSSELET: Mm-hmm. Basically, you can think of when you define a provider, it's basically an observable object. There is an observable object under-the-hood, and there are some ways to listen to changes in that observable object. So one of the way that you can do that is, once again, using the ref object, you can do ref.listen. It's a low level API for listening to a provider. And then you have a callback invoked whenever the provider changes. It takes-- it produces value in the new value and you can print something in here. And the basic idea is watch is effectively this and doing that state. So if you do watch, like if I work to implement watch, T watch T provider, it would effectively do something like that. And then you return the current state of the provider, so ref.read(provider), something along those lines. Obviously, it's very simplified for the sake of the example because I need to be always considering subscriptions and all, but that's the basic implementation. Which is also, speaking of considering subscriptions, that's one of the core reasons why Riverpod was disassociated from provider, because one of the issues with inherited widgets is a widget never stops listening to an inherited widget as soon as you use this once. Say you do Theme.of in here, but only in a condition like if condition. If that condition was true once and then it becomes false, the widget will still rebuild once the theme changes. CRAIG LABENZ: Oh, interesting. REMI ROUSSELET: Yeah, that's an interesting fact. But the thing is, in Riverpod, like with our infinite list example, like the infinite list example, it's a very neat syntax, very powerful. Like, we can do a lot in a very small amount of code. But we are relying on a parameter which changes over time. And so basically, what this means is if I were to effectively unwrap this without using parameters, if we were to use inherited widgets and dumbing down the example, it would be like if pages equal 1, then you do fetchItemProvide r.firstpage(context) and Of. And then each page is equal to [INAUDIBLE] the second page. The thing is, if the page change from 1 to 2 and you stop listening to the first page-- CRAIG LABENZ: Right. REMI ROUSSELET: --the first page would still be listened. But the thing is, it's not used anymore. So the first page would still be maintained and the state would basically be always in memory, even though it's not used anymore. So this is a core-- it clashes with this ability of passing parameters to providers. And passing parameters to providers are all significant simplifications of our UI, as I showcased previously. And so for this reason, Riverpod couldn't use inherited widgets for updating UIs, and it would have to use lower level things, lower level things. CRAIG LABENZ: Yeah. OK. REMI ROUSSELET: So that's why we are not doing-- yeah, that's why we're not doing context.watch like we did in provider, but instead ref.watch, to work around this issue. CRAIG LABENZ: Ah, interesting. OK. I want to summarize this as well, to both check my understanding and help make sure everyone's keeping up. So inherited widgets, they establish a connection between the widget where you write-- and technically, it's the elements behind the widgets, but we'll pretend that's not relevant. So they establish a connection between the widget where you write, that line of code themed out of context or whatever, and the thing above it. So in that example, of course, the theme widget. And the thing is, they're basically-- it's like a stream that never unsubscribes. So when you first write that method or you first write that line of code, the widget where you write the line of code is added to a registry that the Flutter framework is keeping track of-- all the dependents of each inherited widget. And there's no way to get yourself off that list. So if you just briefly need to listen to some inherited widget, you will in fact listen to it forever until the user navigates so far away that your stateful widget is completely destroyed. And so because you are not using that machinery, because you're maintaining your own registry and your own stuff, you're able to functionally have that unsubscribe that inherited widgets are missing. REMI ROUSSELET: Exactly. Effectively, the way it works is Riverpod keeps track of what's used inside the build method. And so once the build method completes, it knows OK, I stopped using this specific provider, so I can stop subscribing to this provider. CRAIG LABENZ: Mm. Wow, so it-- wait, OK, I want to hear that again. I was still thinking about a previous thing. You said when the build method completes, the consumer widget unsubscribes? REMI ROUSSELET: Basically, it's in the element side. There is the consumer element somewhere, consumer element, which extends-- I don't remember which component element. Yes. And so in the component element, component element which is basically shared between stateless widget and stateful widget, it has this build method which invokes the widget's build method. So what Riverpod does is it does a try finally around it. So it does super.build here, which will invoke the widget build method. And so in here, basically-- how to say it? Let's say we usually have a list of dependencies-- dependencies-- which is basically the dependencies used by the build method. And so in here we copy the previously listened dependencies. CRAIG LABENZ: Yeah. REMI ROUSSELET: And then on the build method, after here-- CRAIG LABENZ: You can-- REMI ROUSSELET: Like when we invoke watch-- CRAIG LABENZ: --clear them all out. REMI ROUSSELET: Right. Yeah. When we invoke watch, we start listening to a specific dependency, so it adds it. And so in here, now I have the new list of dependencies and the old list of dependencies, and I can compare both lists and know if I stopped listening to a provider. CRAIG LABENZ: Got it. OK. REMI ROUSSELET: It's a bit [? long ?] once again, but you shouldn't need to care about this. CRAIG LABENZ: Yeah. In the end, it just means that there are specific scenarios where unnecessary-- where Riverpod will avoid some sneaky, unnecessary rebuilds. REMI ROUSSELET: Yeah. CRAIG LABENZ: That's essentially what it amounts to. REMI ROUSSELET: Mm-hmm. I mean, in the context of Flutter, like for most inherited widgets built inside Flutter, the issue I mentioned is not that big of a deal because it's just plain-- it's just basically team data. It's always available. It's never really destroyed as team data on the Material app level. But the thing is, with Riverpod, our state, it's much more precise than just a team always available, never really changes besides maybe when you go into dark mode. It's like I said, the current page, the page can change as often and all, and you want to destroy this state as soon as possible. So you cannot afford to keep it more. You cannot afford to keep it longer than necessary in memory. CRAIG LABENZ: Yeah, that's a really interesting point where, if you look at a lot of the inherited widgets in the framework-- I always close my eyes when I'm trying to work something out to spare my brain the distraction of its visual field. When you use an inherited widget that's in the framework, it's something, like, fundamental about your app, like the screen orientation or yeah, like you said, the theme or-- and there are things that make those change, of course, if the user just rotates their wrist. But a core assumption of those things is that of course it's fine to rebuild everything, or if it's a web or a Mac, a Windows-- a desktop window, they drag and change the size-- of course it's OK to rebuild everything because nothing we previously rendered is safe, if something that fundamental about the screen, about the interface changed. So it's fine to just be like, yeah, we're going to re-render everything. But in a state management solution, that would cause just an absurd amount of rebuilds. And so potentially, the issue came-- REMI ROUSSELET: No. CRAIG LABENZ: --if I'm really-- oh no? REMI ROUSSELET: It's not about rebuilds. It's not about rebuilds. It's about the fact that in Riverpod, Riverpod makes sure that that state is preserved in memory only if it's used. But the thing is, if a widget's never stopped listening to some piece of state, it's always considered as used. And so it's still in use for River-- Riverpod still thinks that it's used, even though it's not actually used, and so you cannot destroy the state. CRAIG LABENZ: OK. REMI ROUSSELET: Whereas by fixing this, we actually can destroy the state as soon as it's not used anymore. CRAIG LABENZ: Oh. REMI ROUSSELET: Yeah. So yeah, it has nothing to do with rebuilds because they are actually-- CRAIG LABENZ: OK. REMI ROUSSELET: Providers, the provider package, actually has some solutions to filter rebuilds already. And there are various optimization factors. It's not all about this. CRAIG LABENZ: Not about rebuilds. OK, it's about not having the runtime memory footprint of apps-- REMI ROUSSELET: Yeah. CRAIG LABENZ: --grow-- only go up. REMI ROUSSELET: Yeah. It's-- think about, it could even be costly to not necessarily just memories. Like, say you use Firebase and it's doing a query, but you change page so the query, it's not useful anymore, but it keeps updating in the background because it's still considered as used. That would be fairly inefficient, and you would want to pause this query as soon as your user leaves the page. CRAIG LABENZ: Yeah. Yeah. OK. Right, right. Yeah, you said that wouldn't only cost the memory-- what if you use Firebase? I'm like, yeah, then it will do the real version of costing, which is-- REMI ROUSSELET: Yeah. It [? costs ?] [? you ?] network resources are so very small in general. Even if you don't use Firebase and you use your custom API, and maybe you do some HTTP polling to refresh some API every X minutes because why not? If the user opens 10 pages, and you don't pause, you don't stop listening to your API if it's not visible anymore, you would suddenly be refreshing 10 APIs at the same time, even if you only care about one. CRAIG: Mm-hmm. Mm-hmm. Wow. OK. That's super interesting. I had not really appreciated that that was the nature of-- I mean, I guess when you talk about disposing, I think, when Flutter developers think about disposing things in general, you talk about disposing a stream, for example. You've got to close your stream subscriptions. That is certainly about wasted network requests, maybe wasted memory, certainly wasted background process. But I hadn't realized that we were kind of getting into the same territory by avoiding inherited widgets and using the [? ref's ?] machinery instead. REMI ROUSSELET: Yeah. CRAIG: Interesting. REMI ROUSSELET: I can actually also pivot into another interesting thing you can easily do in Riverpod is when you think about network requests, you want to make sure you stop them as soon as possible, too. If, say, your user-- your application has a detail page. And the user open a detail page and leave it as soon as he opens it because maybe he doesn't actually care about the digital page-- he made a misclick, and it changed again. Chances are when he opened the detail page, it started a network request to obtain information about that product. But the user doesn't care about it anymore, because it already left the page. So if you have a slow connection, the network request is still pending. But it's not necessary anymore. So you likely want to cancel that network request to save as much resources as possible because maybe the user will want to open a separate product detail page. And you want to show those informations first instead of the old one. And so if you wanted to, say, cancel network-- so if you wanted to cancel network request in Riverpod, chances are your network request implementation would be something along the lines of if you use a dio package, you would do dio.fetch('api'). And I don't know, json. CRAIG: Yeah. REMI ROUSSELET: Then you say, maybe, return Whatver.formJson. CRAIG: Yeah. REMI ROUSSELET: Lots of [? titles ?] or whatever. And so if you want-- so that would be your [? business ?] logic for fetching something. And so if you wanted to add a network request cancellation, you could do these extra lines. Using dio, you would do cancelToken call CancelToken. You pass it to dio. CancelToken here. This CancelToken object would be an object where you can do cancelToken.cancel here to cancel the network request if it's pending. And so the thing is you can invoke this cancel method on something called onDispose, which provides a lifecycle. This onDispose lifecycle is called when the provider stops being used. And the state is destroyed because maybe the user left the page. And so it's not used anymore. So we dispose the provider. And so when we dispose the provider, you can also hook your cancel at the same time. And so if the operation was pending at that time, then the network requests will be canceled with just this line. CRAIG: Interesting. Yeah, that's quite efficient. And I'm not going to lie. I've never canceled a network request in my life. REMI ROUSSELET: Yeah, and it's a fairly advanced topic. But if you start thinking about it, like if you wanted to do it with-- say you wanted to do some HTTP polling, for example, and refresh your API every five seconds, first one thing you would do is-- in Riverpod, doing that is fairly easy. Let me do a timer-- I don't-- timer, your duration, so five minutes, and then ref.invalidateSelf, which is basically telling the provider to refresh itself. So in five minutes, refresh the provider, and it will refetch this thing. And so basically, this would be a request which updates every five minutes. You could do the same thing in your UI. Maybe you want to do a refresh indicator-- RefreshIndicator, in here, onRefresh. You can do ref.invalidate some provider, which will refresh the provider once again using a [? portal ?] refresh. Anyways, the point is when the provider refresh itself, it will invoke this onDispose function again. And so somehow, if, for some reason-- if we take, again, the [? portal ?] refresh example, and the user wants to do a RefreshIndicator-- onRefresh, rev.refresh-- invalidate. If, for some reason, the user refreshes twice because it's very-- you know how users are. They refresh twice at the same time because he wants that data. Then you wouldn't want to fetch the network request twice. That would not make sense, whereas by doing that, you would only fetch it once by canceling-- CRAIG: So this-- REMI ROUSSELET: --the network request. CRAIG: This would decisively mean that anyone who's anxiously swiping again and again and again, they're just resetting their progress. So they're slowing themselves down. The other way-- REMI ROUSSELET: Typically, yes. CRAIG: --to handle this would be to throttle or debounce or something, right? So you only-- REMI ROUSSELET: Yeah, yeah. CRAIG: --refresh once every five seconds or something. REMI ROUSSELET: But anyway, I wanted to talk about this because if you compare it to-- say you want to do-- what's the word-- polling using another approach, you'd have your state here. And maybe, in your constructor, you do your Timer.periodic. And in here, chances are-- what's the word I'm missing? Actually, I actually thought about this. But I wasn't sure how to follow it. CRAIG: Well, it's OK. REMI ROUSSELET: Yeah. CRAIG: I don't think we're-- [INTERPOSING VOICES] --deep into this. Yeah, yeah. I think maybe I'll take one last scan of the questions, and we could potentially move into closing thoughts and get on with our day. I know your dinner is currently waiting for you. REMI ROUSSELET: Exactly. CRAIG: So let's see here. I'm skimming. I'm skimming. I'm trying to find one that I think would make sense to build on what we've been talking about. "If I want to mutate data"-- oh, this one's interesting. There's a lot that's interesting. No one else be offended. But Kevin says, what if I want to mutate data that's coming from a Future on a client, but I want to have an optimistic update? Would you recommend to use a "noterpifier"-- NotifierProvider that holds the state? Yeah, optimistic update, I think. REMI ROUSSELET: For context, NotifierProvider is very specific. It's an actual class in Riverpod. And the answer is no because chances are, you change from a FutureProvider, and you're converting into more like-- if you're familiar with it== more like a StatNotifier, which is a lot more iterative. By using a NotifierProvider, you're basically defaulting to doing things by hand. You start a list. You do future.zen. You catch the error and all. It's very tedious. Instead, what you should use is, if you're not using the code generator, AsyncNotifier instead, which is basically, if I write it here, class Example extends AsyncNotifer Response. Say build method. And so basically, this is your future provider. And you can take the exact same logic in here. And now you can do your update functions. I don't know-- addTodo because it's a list of to-dos. You can do your optimistic updating here. You add your to-dos, and you do your network request like post, whatever, say todo.toJsom. And if some [INAUDIBLE] fails, maybe you can do a try catch and revert it to the previous type if necessary. CRAIG: Mm-hmm. Mm-hmm. Mm-hmm. REMI ROUSSELET: That would be how you would do optimistic updates. And we see generator syntax. The AsyncNotifier is basically-- you just do your usual class. You just annotate the class with @riverpod. And you keep your build method. Everything is the same. CRAIG: Nice. Yeah. So that feels like a pretty standard optimistic updating logic, right? It's just decorated by-- surrounded by a bunch of Riverpod stuff. REMI ROUSSELET: Yeah. CRAIG: OK. REMI ROUSSELET: There are actually some-- there are actually some upcoming features for this, which is the [? imitation ?] issue. It should be coming in the following months. So stay tuned on this. But, yeah, for now, that's the basics. CRAIG: Oh, here's a good one. Oat asks, "So what's the point of using keepAlive now that we have a separate method to dispose the riverpod? The way I'm understanding this question-- my inability to answer this question is partly what made me think, oh, that's a great question. You know, if there's a method to dispose them, you know, that suggests that they're not autodisposing. But then, if there's a method to keep them alive, that suggests that they are autodisposing. So what's the deal? REMI ROUSSELET: Well, the thing is in everything we've discussed so far, at all points, the state wasn't disposed. We never manually disposed the state. At best, we forcibly forced the state to refresh. But we didn't just destroy it forever. We triggered a recomputation of the state. That's all. And so keepAlive-- how do I answer this? CRAIG: Oat has stumped the staff. REMI ROUSSELET: Yeah, just lost my train of thought. Basically, you don't invoke the dispose method yourself. Riverpod does it. It's more like what we've seen is you force it to refresh. You don't dispose it. That's not quite the same thing. Is that clear? CRAIG: Mm. So do you know what Oat means when they wrote "now that we have a separate method to dispose"? Maybe there's some confusion there. REMI ROUSSELET: Yeah. That's what I said. It's that we don't really have a separate method to dispose this thing. CRAIG: Yeah. REMI ROUSSELET: We actually never did. CRAIG: OK. [INTERPOSING VOICES] Do you know what they might be thinking of? REMI ROUSSELET: Yeah. I think they're referring to when we called-- where's my provider? CRAIG: Oh, invalidateSelf or something? Was that-- REMI ROUSSELET: Yes. CRAIG: --what it was? REMI ROUSSELET: Yeah, in the timer. CRAIG: Yeah. REMI ROUSSELET: --ref.invalidateSelf, or when we did this in the UI in the refresh indicator. CRAIG: Mm. REMI ROUSSELET: Technically, this will invoke the onDispose lifecycle. It disposed the state, but it creates one right after it. It's not like the provider is destroyed forever. It's still technically running. CRAIG: OK. All right. Scrolling down more, I'm near the end of the list. Here this is an interesting one, and I think it's probably a good place to call it. Nurbol asks, "Can anyone post some links to repos that utilize Riverpod with good practices?" I would say-- so you can't easily post links in YouTube comments. The YouTube auto comment moderation machinery doesn't really like that too much. But this-- oh, Remi may just be sharing some right now. REMI ROUSSELET: Yeah. You can go to the riverpod.dev website, which contains a bunch of official examples and some examples made by the community. You can look into those. CRAIG: Mm. REMI ROUSSELET: And so maybe inspect your source code for inspiration. So that's [? probably ?] a good source of information. You can also join the Discord, too. If there are any questions about it, click here on the [INAUDIBLE]. It will redirect you to the Riverpod Discord. I'm not connected on the web-- CRAIG: Nice. REMI ROUSSELET: --but that's the ID. And you can ask questions here if you want. People can answer good practices. And also, the linked package technically should help you with good practices in general. In many cases, it will spot mistakes. So make sure to install this one, too. CRAIG: Yeah. Goodness, I do feel like a teammate that I had in my previous company who was extremely experienced-- really, really good-- he would often summarize a lot of these things as, like, what do you want to complain about today, too much boilerplate or too much magic? And all solutions out there have to pick somewhere on the spectrum of boilerplate to magic that they ask their users to write. And I think Riverpod, you've pretty clearly, especially as you're moving more into code generation where you're continuing to hide the boilerplate, you're taking a strong stance on magic. And that just means there's a little learning curve. But then I think, once someone kind of understands what's happening behind the scenes, even if just vaguely, now you can write so little and do so much. And it really is very cool. REMI ROUSSELET: To be honest, I don't quite like the word "magic." I would like-- CRAIG: I thought you'd find it-- REMI ROUSSELET: No, it's fine. I understand where you're coming from. But in my point of view, Riverpod tries to actually not have that much magic. Maybe the generator is debatable on that topic, and I can [? agree ?] with it. But to be honest, the [? Flutter ?] [INAUDIBLE] code generation is more of a language issue than anything. CRAIG: Mm-hmm. REMI ROUSSELET: Riverpod is just kind of limited by Dart restrictions on that sense because if we had phraseable function overloading in Dart, we probably wouldn't need code generation in that context. But anyways, that's getting in a tangent. Riverpod takes a lot of care in making any form of logic very explicit in the sense that, for example, if you were to compare, without naming names, in Riverpod, you explicitly watch something, whereas there are some alternatives where you maybe have some state, some object, and just reading the value automatically flags the widgets that's needing to update the value when the state changes, if you're following. CRAIG: Yep, yep. REMI ROUSSELET: Yeah. And so that, the approach of automatically listening to this thing, in my opinion, would be more logical, as per the definition, whereas here this is a very explicit approach. It's just that, in Riverpod, rather than being magical or not magical, it's more we're using a different approach than most common approaches. As you see, we didn't really [? made ?] a ChangeNotifier and called notifyListeners and all. So yes, there is a learning curve because we're different from usual. So people might not be familiar with it. But the boilerplate reduction comes more from the fact that it's a different approach. And the different approach enables us to go slightly further than other approaches, if that's clear. But, of course, I'm biased. If you think that's magical, that's fair. I cannot counter you on that. CRAIG: Yeah, and there's just things like lots of really smart caching is happening. And even if you never thought about it, stuff like that-- I know that in a lot of circles, the word magic is basically a swear word in programming. I grew up on the Django ORM. And there's basically nothing more magical than that piece of software. And it's one of my favorite that I've ever used. So I don't personally see it as a swear word or use it as a swear word. But I know it's interpreted differently by everyone. But, man, Remi, I had a blast. My brain is leaking-- REMI ROUSSELET: Me, too. CRAIG: --out of my ears right now as I-- REMI ROUSSELET: I'm sorry. CRAIG: No, it's OK-- as I've done my best to keep up. I think I was keeping up to varying degrees throughout the chat. There were some moments where I was just like, what are you talking about, and others where I was right there. So folks, yeah, I mean, if you found this useful, certainly join that. Join the Riverpod Discord. If you have outstanding questions from here, if you saw some code that didn't quite make sense, and maybe I glossed over it or the explanation just didn't land, talk about it in that Discord. I think there's going to be a lot of people there that are excited to go into the weeds on how Riverpod works and also how to use it, right, because this has been very academic. Going behind the scenes, the whole point of these libraries is that you just need to know how some of the methods-- you need to know what the methods do, when to call them, and how they're going to behave. The implementation is really extra credit. It's very, very academic. But I thought it was really fun. And personally, I like to know how libraries work before I use them. It helps me understand when to call a given method. So, Rémi, thank you for all the work that you put into these packages, the linting, too, to save other people's time, starting all the way back in Provider where you were just kind of identifying friction points that you were experiencing developing in Flutter. And you've just always been someone who shares your solutions with the entire community. And, man, we've all benefited from your late-night and weekend thinking about how to efficiently build a Flutter app. And from, I think, all of us to you, thank you for everything that you've given us. We really appreciate it. REMI ROUSSELET: Thank you. I really-- CRAIG: Yeah. REMI ROUSSELET: --appreciate it. CRAIG: Well, folks, I'm going to-- I think we can wrap everything up there. But thanks for joining. And as always, be sure to let me know what you'd like to see on future episodes of "Observable Flutter." Until then, see you, everybody. REMI ROUSSELET: Bye.
Info
Channel: Flutter
Views: 52,044
Rating: undefined out of 5
Keywords:
Id: BJtQ0dfI-RA
Channel Id: undefined
Length: 105min 37sec (6337 seconds)
Published: Thu Mar 02 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.