Keeping it local: Managing a Flutter app's data

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

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).

👍︎︎ 1 👤︎︎ u/pegasi320 📅︎︎ Dec 11 2020 🗫︎ replies
Captions
[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]
Info
Channel: Flutter
Views: 45,724
Rating: 4.9313726 out of 5
Keywords: Flutter app, flutter app data, app data, managing app data, store data locally, storing data locally, persistence mechanisms, strategies for storing data locally, local data storage, flutter, flutter developers, google developers, developers, developer
Id: uCbHxLA9t9E
Channel Id: undefined
Length: 44min 57sec (2697 seconds)
Published: Thu Dec 10 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.