[MUSIC PLAYING] ANDREW FITZ GIBBON: Hello. Good morning, good evening,
or good afternoon, everyone, depending on where you are. Welcome, and thank you
for joining me today. Hi. My name is Fitz. And today's lecture is
going to be on data locality and keeping things local. So let's get to it. So the question is, when
was the last time you thought about your apps data? And I mean like really
thought about it. Where did it come from? What was its journey like
going from there to here? Where is here, exactly? And for that matter,
where was there? Pretty much all of our
apps use data in some way, whether it's some photos
or other media, product information, or creating
data to be rendered and drawn in interesting
and colorful ways. Without some kind of
information or data, an app would be
somewhat uninformed. And often when
data is mentioned, the first question that
comes up is what about state. And it's an important question. It doesn't take very far
into your Flutter journey before you're converting
a stateless widget into a stateful one and
wondering what that state object is right
before you tumble down the winding path that
is notifiers, blocks, and state management. Like data, without
state, an app wouldn't be very interesting or useful. Knowing how to manage it, how
it flows through your app, and when to just use
set state versus a value notifier versus
package provider-- these are all important and
useful topics to pursue. Those fancy graphics wouldn't
draw themselves without state. But this is not that talk. And in fact, for the
entire rest of the talk, you won't hear me
use this word again, because this is
a talk about data and about managing your
app's relationship with data. And so when you're looking
at building an app-- and it doesn't matter what
framework you're using. There are a number of
questions to consider when thinking about data. First is how much do you need. And by that, I mean how
much is critical to your app versus some things
are OK to wait for. Second, how stable or
ephemeral is your data? And by that I mean is it
going to be viewed or accessed repeatedly? Then maybe it's stable. Or is it only going
to be accessed once and probably never again? We'll call that ephemeral data. And third, what is the
true origin of your data? Where does it come from? Is it generated locally
by someone actually typing in some data? Is it generated by other users? Maybe other people
are taking photos. Or is it stored or generated
by your own backend? And finally, where is your
data going to be stored? And for that, it's easy to say
let's just go to the cloud. After all, with
the cloud, our app will have data everywhere
at all times no matter what. Until we don't. Until we don't have that. Since our devices are out
and about on the internet, sometimes we have
connectivity problems. And sometimes things just
run a little bit slower than we'd like. And so on that last
point, I'm curious how long it takes for us
to get data from the cloud. And when you ask the internet
that question, how long does it take to get data
from the cloud, there are a lot of disagreements. What's the important metric? Are we measuring
latency or bandwidth? Are we measuring something else? How do we measure that thing? Where do we measure it from? What services do we use? What kind of data do we use? How is that data
interpreted, parsed, or used on the client side? And generally, what
assumptions are we assuming about the system? There's all kinds
of disagreements. And every benchmark
you look at is going to be slightly
different in some way. So I ran my own. I went to the cloud. I went and pinged
cloud.google.com. And I was attempting
to see how fast I could get from here to there. And the results
were encouraging. On average, depending on
if I was wired or wireless, I was getting less than
20 milliseconds of ping. And that's great. Generally, we're looking for
response times less than 100 milliseconds or so. And so less than 20 seconds,
awesome, except that's ping. And it's also highly variable. On the real internet when
we're looking around the world, depending on where you
are, where you're going, and how the weather
looks like today, you can get pings anywhere from
less than 10 milliseconds up to well over 300 milliseconds. And so already we've blown
by our recommendation of 100 milliseconds
for good response time. But even then, it's
still not real data, because it's still just taking
a random network packet, and sending it on the
network, and seeing how long it takes to come back. It's not actual
application data, because when we're looking
at actual application data-- say we're going from our
phone to the actual cloud-- there's a few more round
trips involved here. We have to ask for data. We have to authenticate. We have to go and find the data. The service has to
go and find the data. It takes a little bit more
time to actually retrieve all of our data. And so when you look out in
the wide world of cloud data benchmarks, they're out
there all over the place. You can see anywhere from tens
or hundreds of milliseconds up to potentially thousands
of milliseconds depending on the service. So I'm going to
split the difference and say one second, that
we have one second where we'll be retrieving cloud data. So what can we do in one second? Well, in one second, I can
download about 30 megabytes on my home internet. It's decent. Or I could read
about 500 megabytes from a standard two and
1/2 inch solid state drive. It's already quite a bit better. That's a lot of
data in one second. Or I could read 96 gigabytes
from my CPU's L1 cache. And of course, that one has
to be fast because the CPU itself can process about
120 gigabytes per second, depending on your
CPU and depending on how you calculate it. And so we can do a
lot in one second depending on what
we're looking at. The solid state drive already
is way faster than the internet. And then when we're looking
at actually processing data locally, it's much, much faster. And just for fun,
in that same second, the current fastest
supercomputer in the world, Japan's supercomputer
Fugaku at the RIKEN Center for Computational Science,
has a peak computation rate of about half an exaFLOP. That's 17 billion times faster
than my home internet trying to download something
from the cloud. So we can do a
lot in one second. So what am I going to
do with one second here? I'm going to go and fetch
a single flag, one bit. Just true or false,
whether or not we should turn dark
mode on in our app. And so you can see,
this is the example app that we'll be working with. And the Material app is there
waiting for that dark flag to come back. I'm using Provider and
it has a dark notifier, which is notifying me of if I
should be in dark mode or not. And this entire app is
waiting for that flag to be true or false,
which is waiting for the existence of that flag. And you can see
in this animation that, after about a second
or so, 0.959 seconds, we get that flag,
and we can actually start rendering things. And of course, we see the images
are running slowly as well. We'll get to that in a moment. And so this is the app
that we'll be working with. And for this first one,
I want to ask our first-- go back to our questions about
data and our first question, how much data do we need? And I'll pose a
follow-up question to this, which is how long does
that one second compare to how much data you're retrieving? For example, in this app, I
am blocking the entire app on loading that single bit,
that one flag, that one true-false flag. And it's super important. We need it, because
we don't want to have this weird bit that's
happening where it starts off in light mode and flashes over
to dark mode really quickly. That's not a great
user experience. We'd like it to be
available immediately. And so that bit is
super important. But compared to what we
could be doing in a second, we could be
conceivably retrieving millions or billions of bits. So that's millions or
billions of times more data than we are retrieving. And so for our one
bit, that starts to feel like a little
bit of an eternity. And so we're going
to change that. We're going to not pull
it from the network, not pull it from the internet,
but instead use a package called shared preferences. And so we're going to pop
over to the code here-- hi-- and actually play
around with the code. So you can see here, on
the right, I have my app, and of course I can restart it. And we'll see in my back-end
logs that we got the dark mode. And it took about a
little less than a second for that dark mode to appear. And it's loading. The images take a while to load. And then you see up
here, I've got my run app is creating the dark notifier. And, same as in the
slides, the dark notifier is here waiting for it is dark. So let's go and look at
that dark notifier thing. We're storing that
in its own data class to just be able to
keep a handle on it. And here, when we
create it, we're starting with this method
of loading the dark mode preference. And that is using
the http client and calling out to our back-end
host to get our preferences, decoding that JSON,
and then parsing it out before notifying
all of our listeners. So this is the part
that's running slowly because, well, you can see that
our back end is taking almost a second to actually
render this. So I'm going to replace all of
this with shared preferences. And shared preferences is
just a local storage option that is based on
key value pairs, where I can have
some sort of name and it'll just
return me a value. That package stores
things on the local disk for whatever system this is. So it'll make a
different decision depending on if we're on Android
versus iOS versus desktop, it'll figure out where the
proper place to store this is. So this part that's running
slow, I'm going to delete it. And we're going to use
shared preferences instead. Note that I've already
imported this package. And I've already added
it to my pubspec.yaml to get this imported. Note that when I
first did this, I had to re-clean and
recreate the Flutter project in debug to get
that to autocreate things. And so, we've kind of
already got this set up, because it's an
asynchronous function that before was using HTTP client
to actually get our data. And we need that to
still be async as well in shared preferences. Because if you remember
that earlier graph where the internet speed,
30 megabytes download, the solid state drive
was at 500 megabytes. And it was way-- it
was a huge difference. But then, there was a
way bigger difference when we were looking at
CPU processing speed. And so, when we're looking
at what is the app doing, well, even when we're retrieving
something from our hard drive, it's still going to feel
like an eternity to the CPU. So we need to await for it. We need to have
that in an async. And what we're going to await,
at first, is the shared-- we're going to get the instance
of the shared preference. And then, because
this returns a future, so we can chain it with .then. That gives us a
preferences object. And within that object,
here's where we can just access our key and value. And so, what we're
going to do, we're going to say we have a DarkPref. And that's just going to
be our-- it's a Boolean. And so, we can use our prefs
object and get a Boolean. And I'm just-- we can
call this whatever. It's just an arbitrary
string that we can use to identify our value. And I'm going to
call it isDark-- just nice and short. And then, when we first do
this, there's no isDark value. So if it's null, we'll go false. If it's not dark mode. We'll assume light mode is fine. But you will notice up here,
I had this currentPrefs data class to use. And so, what I'm going
to do is currentPrefs is now going to be a new
one of this data class. And I'll keep my
promise from earlier and not say the full
name of this class. And this is going to be--
what did I call it-- darkMode, darkPref. OK, that's all we really need. That's all we need to start
using our shared preference. I imported the package. I got the instance
within an async class. And then, I pull the exact
key that I want and save it for later. And I'm going to hot restart
this so we get it from cold. Notice that in my console
here, we have a bunch of-- we're waiting for these files
to appear, almost as if they're arbitrarily slow. But we weren't waiting for
the darkMode preference. And of course, we're
still light too, because we set it as false. So I'm going to refresh again
so we can see that happen. Our list appears
almost immediately. And there's a slight pause
there that's hard to see. There is a slight pause there. And that's just because it's
pulling from the disk, which still has to wait a little bit. The other thing
that's here is, we have this button that
I can use to switch between light mode
and dark mode. And I just click that button. And right now, when
that's clicked, you can see in the
console logs that it's doing the same call
to the back end to try to save this preference. But right now, that's not
necessarily necessary. And so, instead of
this HTTP client, I would like to save this
preference to the shared pref as well. So again, I'm going to await my
shared preference getInstance. And then, I have
my prefs object. And within that, all
I need to do is prefs. Set Boolean instead. And we need to make sure
we use the same key. I should really pull
this out into a constant. But for now, I'm just
going to put that there. And then, we're going to set
it to currentPrefs.darkMode. And so, that's setting
to what it currently has. And just to make sure that
it is actually flipping it, because you'll notice,
when I clicked on it, it happened immediately. When we go into
our app's code, we have our scaffold and our
app bar, as you'd expect. We have our body, and our
floating action button is this thing. And sure enough, we are setting
our darkMode to not isDark. So we're flipping that bit
when the button is pressed. So we can just go and
set it there like that. And that's all we need. We don't have to change it back. We don't have to return
it, because we've already done that. So now, we're going
to refresh again. And what I'm expecting
to see is no new messages in my backend log about
sending or receiving a dark mode preference. I'm going to wait
for that to load. And I'll click the button, and
we'll flip it back and forth. And sure enough,
no backend message. So great, now our
shared preference thing is actually storing our
dark mode preference. And you can see, all the
cloud images get cycled out to inverted images. That's just what I decided
to do for the dark mode. And so, that's our
shared preference. We've removed that
1-second delay in the beginning of our app and
moved all of that data locally. So now, we have access to
it, basically, immediately. The second thing I want to
tackle here is these images. I scroll up and down, and
there's just these blank spaces where images should be. And they take a while to load. Sometimes, they take
long enough that we have to put in a little spinner
to indicate that it's loading. This isn't a great experience. They take forever to load. And almost as if
we are arbitrarily waiting for them to reload. So that's the next thing
that I want to tackle. So we've handled our
shared preferences-- great. And those network-- those images
are simply using Image.Network. And when you look at the
Image.Network documentation, it has this bit-- this one line sentence--
that says all network images are cached regardless
of the HTTP headers. Now, it's curious because-- and you can see it here in
this animation as well-- that our images don't
appear to be caching. And so, it begs the question
of, what's going on there? Are our images too big? Are our headers
not set properly? That shouldn't matter,
because the documentation says the headers don't matter. And so, the question is,
what's going on here? So let's dive into it. Let's look at what's going on. First, the Image.Network widget
is actually really simple. It just hands everything
off to something called the NetworkImage. NetworkImage is
also rather simple. It hands everything off
to the implementation of a ImageProvider NetworkImage. That one fetches the image and
manages some of the image bytes using the
MultiFrameImageStreamCompleter, which is really just
an implementation of the ImageStreamCompleter. And throughout all
of this, there's an ImageStream class
that's just kind of a little bit of everywhere. And that thing is the thing
that actually has the bits and bytes for our image. And so, if it has our
bytes for the image, that's probably the
one that is caching it. And in fact, you can find, in
its documentation, this line about how the image cache
will consider an image to be live until its
listener count drops to zero. So that's useful knowledge. It says that the cache will
only cache something, or keep something around, if
it thinks it's in use. That's good to know. And that last part
of the sentence is really just saying that
when we first create it, there's probably no listeners. Because there's that brief
period between creating it and attaching a listener. When we first create it, it's
OK to have zero listeners. We'll keep it around,
because we assume that at least one listener
will be added later. But if it ever drops back down
to zero, that's when we say, this thing is probably not used. We can consider it done. We can consider it available
to be recycled and get that memory back. So when we have this list view
where those list items are being disposed of
rather rapidly, it's as if someone set
the cache extent to zero. Those images routinely
have to be recycled. And they're routinely
marked as being able to be disposed
and destroyed of. So that might be fine
for a lot of cases. Maybe, our clouds are-- it's OK for them to take
a little bit to arrive. That's OK. But maybe there are
other cases where it's less OK, where we actually
want those images to persist and be around. So for example-- a
few examples-- maybe we have a book library. And I've already got
the book on my device. And it'd be really
nice if I had the cover image already available. Likewise, maybe I'm a teacher,
and I have my roster or seating chart. And sometimes, class time
gets a little hectic. And it'd be really nice
to have that roster load as fast as possible and
not have something else that I have to wait for or handle. And then, of course, we
could have audio, video, media of some kind-- home videos, personal
photos, online music, podcasts, things like that. And of course, those
ought to be local so I can play them offline. And so this goes into our
second question about data. How stable is it? That is, how often
will you need to be accessing the individual
things, like our images? So let's suppose that
these cloud images-- I'll go back one slide-- these cloud images that
are taking a while to load, let's suppose that I would
like them to not load. That they're going to
be relatively stable. And I would like
those to stay around. Let's try caching
those to disk as well. We did that with
shared preference, because our internet speed
for that one single bit was taking way too long. But even with relatively
larger images, it's still nice to
keep those around. That SSD, that shared-- that Solid State
Drive transfer rate was much faster
than the internet, no matter how much data
we were trying to get. And so, let's cache
things to the disk. Now, I could write my own cache. That is an option
that I could do. But instead, there
are packages that can help us do this for us. So I head back to the code. Hi. And I've already got
my DarkNotifier open. I'm going to switch
back to main. And when we scroll down,
here's our Image.Network. It's just pulling from
the image URL of wherever this image URL is coming from. It's initialized as part of
a cloud image creator thing. That is down here. I'll show the code later. And if is loading, we'll pop
a circular progress indicator on the screen. Otherwise, we'll just
return the image. So I have a helpful
note here to myself. We're going to switch that
out for a package called CachedNetworkImage. You look at
pubspec.yaml, I already have this imported here. And I already have it imported
up top, a CachedNetworkImage. And this package provides a
widget, which is pretty simple. It's really similar
to our Image.Network. And so, I'm going to
just replace this. That goes away. And so, instead of
our Image.Network, I'm going to do a
CachedNetworkImage. And our autocomplete
is helpfully saying that I need
our image URL. Hot reload is helpfully telling
me that I have problems there. So this is all you really need. I've got my CachedNetworkImage,
and I'm passing in an imageUrl. You can see that it does
some helpful things for us automatically. Like, it's fading
in those images. And there's no loading image,
which is probably fine. We might be able
to override that. And sure enough, those seem
to be loading pretty nicely. And in fact, we can
verify whether or not it's actually caching them. We can go and find the
files for this device. I'll go to my terminal here, and
I'll check out Flutter devices. It'll show me that the iOS
device that I'm running on has the identifier
that you see here in the terminal, this long UUID,
this 2B3-something or other. And that's where I am. And I'm going to just
do a find through there and find all of the
JPEG files here. So we've got this directory
lib cached image data. Great, we have a place
to go and find our data. So note, so I'm in
that directory now. And our images are loading
pretty well, barring any-- my laptop is running
slowly, trying to do all of the streaming. So any delay right now
is because of my laptop. So we have all these images. And I'm just going
to delete them all. And now, when we
go back into it, notice that my backend has
started logging these again. And these are taking a
little bit longer than with the cached image to appear. And most of them are back. And we go back up. Our backend has
logged more things. I can clear that to show
that now that we've scrolled through the entire list,
there should be no more calls to the backend. Notice we're not waiting
for any of those anymore. But again, if I delete
them all, and then redo that, sure enough, I have
to go and get new images. So this CachedNetworkImage
sure enough is saving these files to disk and
is getting us a lot of benefit. And it's speeding things
up quite substantially. And there's more that we can
do with CachedNetworkImage. For example, we could
put in a placeholder. Like, maybe I wanted a
circular progress indicator here as well. And what is the
recommendation here? We need it to be a builder. So we'll make it a builder,
which takes some context, which returns a widget. Oh, sorry, that requires
two parameters, which I only need the first one. This is why we refer to notes. And that looks kind
of funky because it's taking up the entire
space that we have there. And so, we can just
wrap this with a Center and get just the small circular
progress indicator loading. Great, so that's a
little bit better. We can also do things
with this, like we can have our fadeInDuration be-- we could set that
to 0 if we wanted to have our images appear,
rather than fading in. So you can see that they're
popping in like that. We can also have
the fadeOutDuration, which would be for when that
placeholder circular progress indicator is going away. You see, it's really
hard-- it might be hard to see on the stream. But it's taking-- it's slowly
fading out on top of the image. Maybe we don't want that. Maybe we do, maybe we don't. But when we do our
fadeOutDuration, we can override that to have it
pop out the circular progress indicator as well. So now, that disappears
immediately too. Great. So that's our
CachedNetworkImage. And that was also really
super simple to implement and to pull that network
those network files in and make sure that when we
are reloading these things, that they stay around. I'll note that this
does take disk space. So if you're on a device
that has limited disk space, it can take things up. And there's ways to
override this and make sure that you're only using
certain amounts of data. So we can talk about how much
data it takes up in memory and provide our own
custom cache manager. This is using a package
called the Flutter Cache Manager to make that happen. But that's it. We've now got our
images loading locally. Awesome. And so, we're almost done. We've got just a couple
things left to talk about. And in particular, there's
this third question that I haven't addressed yet. Where is your data coming from? What is the origin of your data? And how is that data created? And so for that, I want to
run out to the whiteboard. So here, I've got my whiteboard. And let's suppose that I
have this little bit of app. And in this part of the app,
this is the Cloudy Recorder. I have this table-like thing. I've got these columns,
and I've got these rows. And so, I could certainly
put some data in here-- so, maybe dashes, that
type cloud of cirrus. And maybe I last saw it today. And what I'm going
to do with this is, I'm going to add
these shared buttons. I'm also using
another package here. So this is just going
to be a Boolean to add. And what I'm going to do
with that, this shared-- this Save button, sorry-- is going to save
out to a database. Because this table kind of
looks like a database table. So when I click on that,
this is going to call out to a package called SQFLite. And this package
is a package that utilizes the SQLite
standard for just having a local file-based database. So we don't have to
have a network call. We don't have to have-- we don't have to call
out to another process. We don't have to call
out to a cloud service. This is all stored
locally still. So SQFLite takes it
out and stores it to a file-based database. There's my little
file-based database symbol. And so, because this looks
a whole lot like a table, we're just going
to create a table. So within this, when
this is first created, what we're going to do is-- I'm going to make sure I'm
not on the edge of the screen. What we're first going
to do is CREATE TABLE. And that CREATE
TABLE statement is going to look a heck
of a lot like our table up here, where we have our
Name, a Type, and the Last Seen columns. And of course, once we have
that, to get all of our data back, we're just going to
do a simple SELECT star. And of course, depending on
what your data looks like and how you're
using the data, you might want to do this a
little bit differently. But that's going to be the next
thing that we look at doing. And for that, this is what we're
going to call Structured Data. So again, we're saving our
data to our local storage, to our hard disk--
our solid state disks. But this time, instead of
having it be files, just blobs of data, like
images are, this time, we're going to do
some structured data. OK, we'll go back
out to the code. Hi. And we'll click on the drawer. So there's actually
some things here. And you'll notice that
my dark mode actually added some transparency. I don't know, it seemed fun. And we go into our
Cloudy Input Recorder. So we have this cloud recorder. And actually, for
this, I'm going to move it back to
light mode, which we'll have to refresh the images for. But I've got some data here. And we look at the spreadsheet. This is using a package
called Editable. And this is a package that
provides a grid that kind of looks like a spreadsheet. You can click into it
and edit these things. So I can, with my
keyboard, edit those. I can hit Enter to finish it. And they've got these
Save buttons already. But let's look at the code
and see what's happening. So here, we have that object-- this object. And within that, I've
again got a future that I'm waiting for data. Because again,
remember that graph of the things going up and down. Our solid state disk is fast. It's much faster
than the network, but it's still really slow
in comparison to our CPU. So we still have
to wait for things. And here, we're saying-- for now, I'm just
saying that I've got some placeholder roles that
are being returned immediately. This is just this flat
list of map objects. And once those are available,
if they're not available, sure, let's use a
progress indicator and get something there. This is available immediately,
so it never shows up. We'll remap it onto
just a dynamic list to send it over to editable. So this is also a relatively
simple widget to use. You provide it what
columns you have. So we'll go up and look at that. Like our database, we
have some Name, Type, and Last Seen columns
that we're using. And our rows are just
those placeholder rows. Those flat maps that we have. And that's pretty much it. I've got my Save
icon already on, so that I can click Save
when we're ready to do that. Right now, it
doesn't do anything. So we'll get to that. We'll fix that. The first thing here is this. We've got our placeholder rows. That's just stored
already in memory. But I would really like these
to be saved to a database. And the rationale for
this is that maybe this is a mechanism for
recording information and observations about clouds. Great. The best way to view a
cloud and see a cloud is to be in the cloud. And maybe I'm in a
tiny little prop plane, and I don't have any internet. And so, I can't use a
cloud-based service-- ha ha-- because I don't
have any internet. So I need it to
be stored locally. And so, I've already done
a lot of the work here. I have this DataProvider
class down here at the bottom. And this is
utilizing a database, which is coming from
the SQFLite package. And sure enough, here,
when we open the database, we create the table like
we saw on the whiteboard, with our Name, Type,
and Last Seen data. Cool. So I've already initialized up
here in this object as well. And what we're going to do-- our future is just going to
be to get all of our data. And so, when we
pop into that, that returns the future
list of clouds. And it's just doing a query,
getting all of these fields from this database. Then, it's doing some
mapping to translate it into reasonable objects
and returning them out. So let's see what's
in our database. We'll refresh, because
we made some changes to persistent data. We'll go there. OK, there's nothing
in my database. That's cool. And so, like when we
checked for our image files, we can also do the same
thing for this database file. I did claim that it's stored in
a file-based database format. And so let's verify that. So I've searched through this
device for the clouds.db. And I can just use the SQLite
command line to go in there. And we'll check our schema
to make sure of that. Sure enough. Yep, there's our CREATE
TABLE for the clouds. And I can also verify that
there's nothing in there by selecting star from that. Sure, there's nothing there. So the next thing
we have to do is to actually edit this and
provide some data here. So what we can do is
we'll add a row here. This is me. Maybe I'm a stratus cloud. And maybe we last saw me today. Right now, I click
the Save button. Nothing happens because
our onRowSaved is not doing anything at all. Let's make it do something. And so, what I
want to have it do is that this Editable
package gives us the row with all the data,
unless there's no edits made. And what it does
is it just returns us a string that says no edit. And I want to say,
if it doesn't equal that, then I want
to actually insert this row into the database. So I have an insert
function already written. That row is just a dynamic
map of keys to values. So I'm going to take that from
a map and translate it there. OK, let's give this a shot. I'm going to do a
hot restart, just to make sure we're running
from a clean build. And go there, we'll
add our new row. Maybe this one, we'll
have it be dash, which is of type cirrus,
which we last saw today. Hooray. And we save. Maybe we'll save it again. We'll see what happens. Oops. There we go. Now we have two rows. Now granted, I'm not
doing some ID checks to make sure that we're
not getting duplicates. So that's a bug that we will
have to solve in the future. But I do have that data
in this database now. So if I refresh this, we'll
do a hot restart again to make sure I'm
pulling in the new data. We'll go back to our
cloudy data input. And sure enough, there are
the two rows that we've saved. So we'll need to update
this to check for the IDs. But that's going to
be for another time. We now have our data stored
in a structured database. So that's our structured data. And that covers our
three questions. So let's review for a second. We started this
tour with saying, where should my data be stored? That was our fourth question,
and we started there, and we worked our way backwards. And the answer really
is, it depends. So how critical is
your data to your app? And how long does network
retrieval, getting something from the internet, really
feel for that data? Does it feel like an eternity? OK, that's a consideration. And then, how stable or
ephemeral is your data? Is it going to be
viewed just once? In that case, ephemeral,
and maybe it's OK for it to be taking a
little bit to load. Or is it going to be
viewed multiple times? Like that audio book
cover that I mentioned. In that case, maybe
consider locally, so we can rapidly retrieve
it again and again. And finally, where is
the source of your data? If you are observing clouds,
and have no internet, and need to record
and save data, definitely store it
in a structured format like a SQLite database. Just also, don't forget
about the cloud, too. Because once you do have
that internet access back, once you do have that ability to
await things and send them out via a future or an isolate, to
get things saved to the cloud so you can sync
them back up again, depending on where you are-- that is also extremely useful. So that brings us to the
end of my talk today. I'm going to hang out
for just a little bit and answer any
questions that we see coming through the comments. But otherwise, thank you
so much for being with me. [MUSIC PLAYING]
What if the data is also stored in the cloud and could be updated from a different device etc? Would it be best practice to then load the cached data and display it quickly, and make the https request anyway, and then update state if the data doesn't match the cached data? Or should I just not even bother with caching the data and always just subscribe to the stream. (I'm using Firebase Realtime Database for reference).