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.