Codelab: Get to know Firebase for Flutter

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[MUSIC PLAYING] SPEAKER: Hi there, Flutter developers, and welcome to our Get to Know Firebase for Flutter Codelab. Now, over the course of this video, we're going to learn how to add the power of Firebase to a Flutter application. Specifically, we're going to learn how to create a Firebase project, how to install some of the basic plug-ins and configure our app correctly, and then we'll add in some of the more common features. We'll use Firebase authentication to sign in our user and remember who they are, and we'll use Firestore, our NoSQL database in the Cloud, so that we can easily store some information that can be shared across all our devices. Now, before we get started, I'm going to say that this Codelab is generally designed for Flutter developers who don't have any experience with Firebase. It's fine if you've never used Firebase before, but you should at least be somewhat familiar with Flutter. Like you should be somewhat comfortable with creating a basic Flutter app and running it on a device or an emulator, and it would definitely help if you had a general understanding of the Provider pattern. If you're comfortable with all that, then you're good to go. If this is your first time using Flutter, maybe this isn't the Codelab for you. You might want to check out the Intro to Flutter Codelab first and then come back to this one. So with that in mind, let's get started. Now the link to the actual Codelab is available in the description below. So make sure to open that up so you can follow along with this video. Otherwise, the prerequisites here are pretty straightforward. We're going to need an IDE. For the purpose of this Codelab, I'm going to use Android studio, but VS code is also a perfectly fine choice. You'll need the latest stable version of Flutter and a Google account that we can create the project in the Firebase console. And it would probably help if you had a copy of Git running on your machine since that's the easiest way to get the sample project. So with that, let's get started. So the first thing we're going to do is get the sample code for this Codelab. And we can do that by cloning the GitHub repository from the command line. So I'm going to go ahead to my screencast folder but go ahead and pick any folder that works for you. And I'm going to copy and paste this line here. This is going to grab our Flutter Firebase project, along with a few other Codelab projects and stick them all into a folder called flutter-Codelabs. And there we go. Now I'm going to jump into the firebase-get-to-know-flutter directory, and you can see here I have different folders set up for different steps. These are basically snapshots of what your Flutter project should look like at the end of each of these Codelab steps. So if you ever get lost, your project seems broken and you're like, what did I do wrong, you can always compare your working version to one of these other projects and see what's different. But for the purpose of this Codelab and this video, we're going to open up the starter project in step two and keep making modifications to it the entire time until we get to the finished project. OK, so let me open up the Android Studio. I'm going to select Open an Existing Project, and I'm going to select the flutter-Codelabs, firebase-get-to-know-flutter, step_02 folder, and I'll open that up. I'm going to close this dialogue that tells me, Android Framework Detected, but I am going to go ahead and run flutter pub get and after a moment all those red squiggly lines will go away, and I should be able to run it. In fact, you know, let's do that. I'm going to run this on my iOS simulator just to make sure nothing is broken. Now while this is building, let's take a look at what we have here in our main Dart file. So we've got a widget here that's basically describing our event, and that would be this hypothetical Firebase meetup. Now we have two other files here in our source folder. The widgets file basically contains a few helper widgets that just contain some extra styling and padding information. Basically, the Codelab authors extracted some of this information out into separate little widgets that we can refer to in main.dart just so that our main file isn't going overboard with the nested widgets. It makes things a lot cleaner and a lot more organized. And I'm guessing many of you have of seen this sort of thing before. Anyway, we're not really going to touch this file much in the course of this video. So you can kind of ignore it. It's just good to know it's there. Now this authentication file is a lot more interesting. I'll talk about this more in a couple of sections. But this basically contains a widget that changes its appearance based on the authentication state of the user whether they're signed out, signed in, in the middle of creating an account, and so on. You will notice, by the way, there's not a ton of application logic here. It's really about showing the proper sets of widgets to the user. Now when you create this project, you're going to be passing in a few pieces of information about the user, along with several functions that the widget should call when, for instance, the user clicks the Sign In button. This is where the Firebase functionality will come in. Like I said I'll cover this a lot more in a couple of sections. So don't worry too much about this now. Anyway, let's go back to our sample app which is up and running. Let me go ahead and change this line from pizza to sushi to make sure hot reloading is working. And it is-- lovely. And so with that, we've got our application up and running. Let's make it do something interesting, and we can do that in the next section. OK, first up we're going to go ahead and create a Firebase project. Now our project is basically the top level entry point for Firebase. This is where we set up instances of our Firestore database, our authentication services, analytics properties, and so on. Now projects usually contain one or more apps, and in the next step we're going to register both the iOS and Android version of our app to the same project. That will make sure that our guestbook information can be viewed across both devices and that if the user creates an account on an iOS device they can also sign in on Android, and so on and so forth. I'm going to click on this link to head to the Firebase console, and then we're going to click on this big old Add Project button. Now we can give our project a name. Go ahead and name it whatever you want. I'm going to call it Firebase Flutter Codelab. You'll notice that underneath it's giving your project a unique identifier, since there will probably be lots of people creating a project named Firebase Flutter Codelab and that's fine. And you can go ahead and click Continue. Next up, it's asking us if we want to enable Google Analytics. Now this is really useful if you wanted to get any insight into how people are really interacting with your app. It's also useful for features like A/B testing, or getting the percentage of crash for users, or getting better targeting for your notifications, or Remote Config settings-- all of which is great stuff but we're not going to use any of that in this particular Codelab. So I'm going to disable it for now, but you can always add it back in later. And with that, I'm going to click Create Project and wait a few moments. And our project has been created. I'll click Continue. And in the next step we're going to enable a number of different services within our project. By default, a lot of these services are disabled until you explicitly say you want to use them, like our authentication service. So let's start by enabling Firebase Authentication. I'll jump to the Firebase Authentication section of the console and click this get started button. And now we have a list of sign methods to enable. Now, by default, all of these are going to be disabled until we explicitly enable them, and that's probably a good idea from a security standpoint, right? We want to keep our exposed surface as small as possible in terms of potential attack vectors and whatnot. So we're just going to enable simple email and password sign in our application. So I'm going to click this email password option and click Enable up here, and we're good. We're not going to use this password-less sign in method in our app, so we can leave that disabled. And as you can see, there are a ton of other sign in methods you can implement if you want. You can have user sign in with a third-party provider like Facebook, or Google, or Apple. You can have them sign in by sending them a text message, and so on and so forth. Like I said, lots of different ways. But using email password is probably the simplest to implement. So we will stick with that for today. OK, next up we need to enable our database. So I'm going to go ahead and click on Firestore, and I will say yes, go ahead and create a database. There's a couple of options here for how you want to set up your security rules which we will talk about in much more detail later. Production mode is locked down by default, which means that nobody can make any changes to your database until you explicitly allow those actions in your security rules. Test mode means that anybody out there in the internet could potentially read or write to your database for about 30 days. Now it probably goes without saying that this is a terrible idea. It is not very secure, and you should make sure that if you start off in test mode, you definitely lock it down later. And in fact, we are going to do that in a later session. So I'm going to start with test mode for now and I'll click Next. We also need to decide where our Firestore database is going to be located. In general, you should try and pick a location that's close to where you think the majority of your users are going to be. In my case, I am the only user of this Codelab, so I will stick with this default US Central location. And after a few moments it's going to provision a database for me. That's exciting! And with that, we are done. Pretty easy stuff. But if you make a lot of sample projects like I do, sometimes these little provisioning steps are easy to overlook. So if you find yourself wondering why some service isn't responding like you expect, do make sure you've enabled it in your project first. OK, with that, let's move on to the next step. OK, now that we've created a Firebase project, we'll want to add the iOS and Android versions of our app to the project. Now the Codelab says that you only need to do one of these steps to complete the Codelab, and that is true, but I'm going to show you both-- mostly for a maximum educational purposes but also if you're making a Flutter app, being able to publish both the iOS and Android versions is half the fun. So we're going to do a few things here. We're going to add a few dependencies to our pubspec.yaml file like we would any other Flutter library, and then we're going to go onto the Firebase console and register the two versions of our app onto the project we just created. And as I mentioned in the last step, you do want these two versions talking to the same project. Now after we've done that, we're going to download a few constants files that tell our Flutter app a little more about our Firebase project so it knows to connect to the right one. And then we'll do a few little fiddly bits in our Android and iOS specific projects just to make sure all the connections are set up correctly. So here we can see a few of the libraries they're asking us to install in our pubspec.yaml file. Cloud Firestore, which is the database, and Firebase Auth. Also, while we're here, we're going to want to add the provider dependency, since we'll be using that quite a bit. So I'll just copy and paste these. And I am just copying these version numbers directly since I know these are the ones that have been tested and work with this Codelab. Obviously, you can always grab the latest versions from pub.dev if you're making your own project. You can actually find out a lot more about these libraries, including their locations in pub.dev, links to sample code, documentation, and the most recent version numbers by visiting this page at firebase.flutter.dev. It's pretty nice. Anyway, once I've added these I'll want to do a pub get. You always want to do a pub get. And now we got to go ahead and add these apps to the Firebase console. So I'll start with the iOS version. So let's go back into the console here and click Project Overview. I'll click the iOS icon in my project and it's asking for our bundle ID, and to get that we'll want to open up our Xcode project and grab it from there. Now, the Codelab is telling us we can do that by calling open ios/Runner.xcworkspace from the command line. And in fact, let's do that from the terminal here. But if you are using Android Studio you can also get here by going to the Tools menu and selecting Flutter, Open iOS Module, and Xcode. If you're on a Mac, you should be able to see that option and they both do the same thing. So once we've done that Xcode will open up with our project. And I'm going to go ahead and click Runner up here and then Runner underneath the target section here, and right here is my bundle identifier. So I'm going to copy this and paste it into the Firebase console. And I'll give my app nickname, like Get To Know Flutter. You can leave this AppStore ID blank for now. You might need this later if you want like dynamic links to properly direct your users to the AppStore, but you can totally add that in later. You definitely don't need it for now. So I'll click Register App, and it's asking us to download the Google service plist file, and do be careful here. If your file has l a 1 in parentheses after it like this, you'll need to do a little file renaming to make sure it says googleservice.info/plist exactly. Once that's done, I'll drag it into the runner folder here in Xcode. Make sure Copy Items is selected and then click Finish. And then back in the console, you can skip the rest of these steps in the dialogue. These will be taken care of for you by the Flutter tooling. With that, we are done configuring the iOS version. And just for fun, I'm going to run this and make sure everything works OK. Now during this part of the process, it's going to need to run pod install, and depending on your setup and whether you have copies of these libraries already cached locally, this might take a long time to run for the first time. So just be patient. And a few minutes later, here we are now running the iOS version of our app. It's going to look and behave exactly the same as before because we haven't told it to do anything different yet. OK, next let's go ahead and add the Android version. So back in the Firebase console, I'm going to click Add App, and then I'm going to click the little Android icon. This time we're going to need our Android package name and there are a few ways to get this, but the Codelab is recommending we grab it from Android, app, source, main, AndroidManifest.XML, and right up here at the top is our package. I'm going to copy and paste that into the dialog here. Again, nickname is optional, but I think I'll give it a name like Get to Know Flutter Android, and you can leave this debug signing certificate thing blank. We don't really need it here. Now I'm going to download the google-services.json file, and again make sure this doesn't have a 1 in parentheses after it. We're going to put this in our Android app directory like so. It looks like it's still indexing-- let me wait a few moments and we'll try adding it again. OK, better. I'm actually not going to add this file to our Git repository. If we were to look at this file, you'd see it contains a bunch of constants that our project will need so that it knows what database to talk to, what client ID to use, and so on. And I should point out here that nothing in this file is really secret. If I were working with other team members who all wanted to access the same project together, I probably would want to check this into source code so I can share it with my co-workers. On the other hand, if you're working on like an open source kind of app or you want every developer out there to set up their own Firebase project, you should probably leave it out. And so that's what I'm going to do here. Now back in the console we're going to skip through the rest of these dialogues because the Codelab has the next few steps for us to follow. So we can do it there. Specifically, I'm going to open up my project level Gradle file. That's this one at Android/build.gradle. And I'm going to copy this dependency from here into here. This is basically the library that's going to read in and parse that json file for me that I just added. And then let's open up our app level Gradle file-- that's this one here at Android/apps/build.gradle. Yes, we have two different Gradle files. I'm going to copy this apply plug-in line here into this section here. Also, while we're at it we strongly recommend updating your minimum SDK version to 21. That will save you from some weird, multi-dex related, too many methods errors later on. And with that, I think we're done. Now, just to make sure I haven't done anything wrong here, I'm also going to run this on my Android emulator. So I'm going to pause this video since the first time running always takes a while. And here we go. Just like the iOS version, my app runs exactly the same as before since we haven't actually asked Firebase to do anything for us yet. But it does run which is the important thing. So with that, I think we are done with our platform-specific work. So I'm going to close out some of these Gradle files and close up Xcode because from here on out, I'm going to be doing all of my work in Dart. So let's move on to the next step. OK, so let's start signing our user in. Now Firebase supports a lot of different sign in methods but right now we're going to start with a plain old username and password approach since that's probably one of the simplest in terms of implementation. But it does involve a number of different screens or at least widgets that we're going to need to show to our user. Luckily, the creators of this Codelab have done a lot of this work for us. Now I think before we take a look at the code, let's first take a look at our expected sign in flow here at the bottom of this page. So you can see here that when a user first uses the app they're going to be signed out and we're going to present them with an RSVP button. When they click that button, we're going to ask them for their email address. After they enter their email address, we're going to check, is this email associated with an account that's already in our system? If it's not, no worries. We'll just ask them to create an account. They can create a password and add a display name. And if it is associated with an account already, well, that's great, too. We can ask them for their password, and from there they'll be able to sign in. Now after they've signed in that RSVP button will change to a logout button, which they can use to sign out. So with all that in mind, let's take a look at the authentication widget that's going to handle most of the UI work for us. You can see up the top here we've got our ApplicationLoginState, that's an enum, and this describes where the user is in our sign in flow. Either they're completely logged out, we're asking for their email address, we're asking them to register a new account, we're asking them for a password, or they have successfully logged in. Down below here, you can see the authentication widget which displays the appropriate form elements depending on where our user is in this sign in flow. And you can see here that most of the work is being done in this big old switch statement that, for instance, if you're logged out shows this RSVP button, which will initiate the sign in flow. Or if we want them to enter their email address, it will display this email form widget which we've defined below. Here's a form for entering your username and password. And here's a form for registering a new account. And so on. Then down here at the bottom are the actual widgets that we're adding above. I don't think there's too much here that's new or interesting if you're already familiar with Flutter, except maybe notice how they're using the foreign key to help validate all the entries in the form. But let's go back up here because I glossed over these callbacks here. See, normally I like to follow the sophisticated architecture design of cramming everything together into a single giant class. But apparently the Codelab authors are trying to actually separate out our display logic from our application logic. So fancy. So yes, this widget is doing a lot of the work of displaying the proper form elements depending on your user's login state, but in terms of say finding out if a username and password are correct, or registering a new user, or seeing if an email already has an account associated with it, all of that work is being handled by these functions being passed into our constructor along with a couple of bits of info here about our user like their login state and email. So how are we going to pass in all this logic? Well, we're going to create a class called ApplicationState that includes a bunch of application related logic, the function you'll need to call to register a users account, and so on. It's also going to keep a field of this application log in state enum to record our users current login state. And then finally, using the Provider pattern, it's also going to act as a change notifier for this user login state, and later other things, and that way it can notify our consumer-- in this case, our main application-- when the user's login state has changed. When it does, our main application will rebuild this authentication widget, and when it does that it'll pass in our users new login state along with all these methods that our widget needs to call in order to actually perform most of these auth-related tasks. And what are these auth-related tasks going to be? Well, mostly calls to Firebase authentication. Let's see this in action so you get a better idea of what I'm talking about. So let's start with our imports. And I'll be honest, I usually just rely on Android Studio to import the proper libraries for me, but we will follow the Codelab instructions here. We'll import Firebase Core-- this contains a few small functions that we'll need to initialize Firebase. It's kind of brought in automatically when you add in other Firebase libraries to your YAML file. And then we'll import Firebase Auth, which handles all the logic for signing any user, and the provider package. It looks like we need to import this authentication widget, too. OK, so the big thing we're going to do here is create this ApplicationState class, which extends ChangeNotifier. So we've got an init, and this init method is going to do a couple of things. First thing we're going to do is initialize Firebase. That basically involves reading in that constants file we imported earlier. It's technically an asynchronous call, but it happens really fast. And then we're going to set up a listener on the Firebase Auth instance. Now the Firebase Auth library essentially calls this callback when a user's login state has changed. And that's typically either when a user has logged in or they have logged out. So you can see here that we change our login state field, and then we go ahead and notify our listeners that we can change our application view appropriately. Speaking of which, let's define that field down here. We'll add the getter and let's also add the email field since there are points where we're going to need that. OK, so these different functions are the functions that we're going to be passing in to our authentication widget. This startLoginFlow function, for instance, is pretty simple. We simply change our users login state and notify our listeners. So I'll copy and paste that in. This verify email function is a little more complicated. This is the one that takes in an email and checks to see if there are any Auth accounts already associated with it. It does this by calling the fetchSignInMethodsForEmail function, which looks to see if there are any registered methods by which this email address can sign in. Now this password method is the only method where enabling in our app. So if we don't get this back in our list of methods, then we know this is a new account and we can move on to the register new account login state here. And again, we're notifying your listeners. Here's sign in with email and password. It's pretty straightforward. It calls the Firebase function of the same name. Notice there's no log in state switching here. That's because if the user does sign in successfully, that change will get noticed up here where we're listening for a user changes. And if the user enters an incorrect password, that gets surfaced as an error. So it's an important to wrap this up in a try-catch loop so that we can let the user know to try again. cancelRegistration-- looks like we need this if we're about to create a new account but then we change your mind. registerAccount-- this is the method that's going to create a new user. Notice that as soon as this is done we make another call to update our users profile with the display name that we also captured in the form and pass it along to this function. Then that name will be stored along with the Auth object. We don't need to store that separately, which is convenient. And like signing in, if you successfully create an account that will be captured in our user changes listener up above. Finally, signOut tells Firebase Auth to sign out, and this happens immediately; no network calls required. Once again, that change will be noticed and captured by our user changes listener up above. So now that we've added all this logic into our application state let's wire it up to our app. First thing I'm going to do is change our simple main method up here. Inside runApp we're going to use a ChangeNotifier provider. So basically we're defining the ChangeNotifier we're going to listen to here and create. And that is our application state logic. And then inside of our builder, I think this is basically creating the top child of our widget tree that can listen to changes from this ChangeNotifier. At least that's my understanding of things. You Flutter experts can correct me if I'm wrong. But most importantly, down here we're going to add in the authentication widget that's going to change its appearance based on our users login state. And we can do that by creating a consumer of application state. In the builder we're going to create this authentication widget. And you can see that all the values we're passing in are either these bits of info about our user, like their login state or their email address, or all the callback functions that do the hard work when the user clicks the various buttons in this widget. Back in our application state, when we change our users sign in state and call notify listeners, that's going to be noticed right here in our consumer. It'll go ahead and recreate the authentication widget, passing in the updated user login state, and our authentication widget will display all the proper UI elements because of that. So with that, I think we're ready to test. So we're going to run this. And just because we are messing with main here, I'm going to stop and restart this app instead of hot-reloading. Not sure if that's really necessary, but I like doing that if I'm messing with main. It looks like my application is up and running. It says RSVP Here in this button because I'm not signed in, so when I click it, it asks for an email. I will enter test@test.com and because that is an email not yet associated with a sign in method, it asks me to create an account. So I can do that. I'll create a display name of Tom Tester and add a password, and I'll click Save. And with that, I am signed in. If I sign out, I'm immediately signed out and the UI updates itself again. I can go through the log and flow again. This time when I enter my email it knows that it's associated with an account, so I'm asked to sign in. By the way, one thing we haven't implemented yet is the whole, oh, I forgot my password or I want to change my password flow. That's basically left as an exercise to you, the reader, but now that you understand how all this other stuff works. It shouldn't be too difficult for you to add in. While I'm here, this isn't officially part of the Codelab but I am not a fan of the RSVP button being the Sign In button. So let me just change this label here to Sign In to RSVP so it's clear that it's going to initiate a signIn flow. That seems better to me. And just for fun I can try this on my iOS app as well. So you can see that if I sign in as Tommy Tester, I am allowed to sign in here just fine, right? Because they're both talking to the same project, usernames and passwords are consistent across both apps. But I'll create a second account here called Sally Sample. You don't need to do all this, but it will make the next few parts of the Codelab have a little more exciting. So go ahead and create a second user if you want. And with that, we are done with Auth. Let's move on to the next step. OK, here we are in step six-- Writing Messages to Firestore. Now there's a lot of Firestore that we're not going to be able to cover in the course of this video, unless of course you want this to be a 10-hour long Codelab. But the real quick summary is this-- Firestore is a database that lives in the cloud, and it stores all of your data into objects called documents. And documents you can kind of think of as a big old key-value pairs. So you have keys here, and values, which can be anything from strings to numbers to timestamps to JSON-ly looking objects and so on. Documents, in turn, are stored in this thing called a Collection, which contains many documents. And you can search or retrieve documents from within these collections. Now, in our app, we're going to go ahead and create a little message board or a guestbook, I guess, for all of our users that are planning on attending our meetup. So we're going to create a collection called Guestbook and in there we're going to store all of our users guestbook posts as individual documents. So you can see up here we're planning on storing their display name, the text of the message, the time stamp, and their user ID as determined by Firebase Auth. Now there's a ton more to talk about on Cloud Firestore around how to structure data, query data, paginate your data, and so on and so forth. And I encourage you to either check out the documentation sometime or watch this video series to find out a little more information. Although, I don't know-- this guy doesn't really look like he knows what he's talking about. You can't really trust a guy with a thumb that big. So the first thing we're going to do is add in all of the UI elements so that our user can type in their guestbook entry. This should look pretty straightforward. It is a stateful widget that's got a text field and a send button next to it. I guess the only two things they call out in this Codelab is that, once again, they're using a global key of type FormState to add form validation. And there is a link to a very interesting video about keys if you want more information about that. The other thing they note is that this text field here is in an expanded widget just to make sure it takes up as much of the row as possible. Anyway, I'm going to go ahead and paste this entire widget into main.dart. Maybe I'll stick it before ApplicationState. Oh and by the way, if this weren't a Codelab but a real app, I would definitely be putting ApplicationState into a separate file here. I guess maybe they just didn't want you switching around files too much in a Codelab. Anyway, you'll notice that up here the one argument that we're passing into our guestbook is the addMessage function. Basically, this is the function that this widget is going to call when the user clicks Send. Just like with our authentication widget, we're going to separate out our display logic from our application logic. So the guestbook widget doesn't have to know the contents of this addMessage function. It just knows that it needs to call it at the appropriate time. So let's put this into our widget column here. You can see that right now all addMessage is going to do is print out our message to the console. So let's try this real quick. At the time of this recording, I got an error that said, This expression has type Void and can't be used. And that's because in the widget we're saying addMessage is a future void, and right now while we're just debugging it's only returning a void, so I had to fix this by temporarily changing the type of this function. But it turns out the Codelab authors have an even more clever solution, which is to change this type to a future or void, which means it can be a void type or a future void type. That has been pushed out to the Codelab, so you probably won't see this error that I am currently working around. Anyway, now I can reload this. I'll type in a little test message and hit Send, and you can see that our test message has been printed to our console. So we have our UI element in. Let's add in the logic to have it talk to Firestore. So we can go ahead and import Firestore up here, and then in our ApplicationState class we'll add in the logic to write a document. What you might notice is that logic to do this-- it's pretty simple. All we're saying here is hey, you know this collection called Guestbook? Well, let's create a new document in there, and this document is going to consist of these four key-value pairs. You got the text field, which is equal to the message string being passed in; the timestamp, equal to the current time in milliseconds; the user's name, which we can get by looking at the display name of our user, which we stored earlier in Firebase Auth, as well as the user ID. Now we didn't really talk much about user ID, but basically any time you register a user with Firebase Auth and whether that's using something like email password or using a third party provider like Google or Facebook, Firebase Auth will generate its own unique string to represent this user within our application. And we'll make use of this user ID and other places too, like our security rules, which you'll see you in a bit. So now all that's left is to hook it up. Here, let me bring back our original addMessage function here. And then we're going to wrap these original widgets inside a consumer of application state, and we're doing that because we need to talk to our addMessageToGuestBook method inside of our app state. And I guess, also, because we want to show or hide this part of the guestbook depending on whether or not our user is signed in. Remember, this function only works if our user is signed in and we can access things like their display name and user ID, so we don't want to show this for users who haven't signed in yet. By the way, if you're wondering what this three-dots thing is in Dart, it's known as a spread operator, which is kind of a fancy way of inserting one list into another list. So our children array here will either be empty or it will contain this header and guestbook elements. At this point, let's hot-restart our app, and now because we are logged in we can see our Guestbook widget. So I'm going to say something like, so excited to attend, and hit send. What happened? Well, let's see, if everything has gone right, we've created a new document inside our Guestbook collection. And how do we know if it did? Well, we can go to the Firebase console. So I'm going to bring up the Firebase console, make sure it's open to our project. I will check out Cloud Firestore-- and here's our guestbook. It's added a document and here are all the values that we said we wanted. That's great. Let's add another. And if we do that, we can see that it's been added pretty quickly. So how about that? We are now officially talking to Firestore, and this is great. But probably the next thing we should do is read from Firestore and display those messages in our app because we definitely don't want to give all of our users access to our project in the Firebase console so that they can see what other people wrote. So let's head on over to the next section and get that part done. OK, here we are in part seven. It's time to start reading in all those messages that we've been writing to our Guestbook in Firestore. So let's look at how to do that. Now, in general, there are two main methods for retrieving data from Firestore. You can perform simple one-time fetches where you make a call and get back some data a few moments later. But the way all the cool kids are doing it these days is to get your updates in real-time by setting up a real-time listener. Basically, you're going to set up a query in Firestore, and then you're going to tell the Firestore client library, I would like to listen to the results of this query. And when you do that, what's going to happen is you're going to get back the initial batch of data, but then any time any of that data changes, either because an element has been modified or a new document has been added or so on, that data will get sent to your app in near real time. And using some of the techniques we're going to follow in this Codelab, you're going to be able to show those changes in your app right away. It's a pretty magical experience when you see it happen. So let's get started making it. So first things first, let's go ahead and import this async class here, since we are going to be doing a lot with streams and whatnot. And then it looks like here we're going to create a very simple GuestBookMessage class, which really just consists of two strings-- the author name and the message. And we'll just put that here. Now we're going to start doing some of the real work in our ApplicationState. First we're going to add a field of guestbook messages, and you can see that's a list of this GuestBookMessage class, and that's going to be private. So we're adding a getter right underneath. And then here we have a stream subscription of QuerySnapshot. This is basically a reference to our real-time listener along with the logic that we're going to call every time we get new data. Now the reason we're keeping this reference around is because that at a later point we're going to want to be responsible app citizens and turn off this listener to save data and resources. Also, honestly, I'm not sure what happens if we don't keep a reference to it in our class. Does it get garbage collected? I honestly don't know. If you know, let me know. So we're going to go back up to where our listeners are set up to listen for changes in our users off state. Now what we're going to want to do here is listen to these Guestbook documents once the user has successfully signed in. So here's what we're going to do. Using our instance of Firestore, we're going to look at the Guestbook collection. We're going to order it by timestamp in descending order, meaning we're going to get the newest first. This part here says we want to get the snapshots for this data and add a listener onto it. So here snapshot contains a property called docs, and docs represents the list of documents that are being received from the database. And so by fetching the data of each of these documents snapshot objects we can get the data of these documents in convenient key-value form, or I guess more accurately as a map. It's in this listener that we can do the work of filling out our Guestbook messages. So we'll start by emptying out Guestbook messages, and then we can iterate through each document, grab the data, and create a new GuestBookMessage object using the values for the name and text keys that we find in our document data map. And then, after all that, we can notify our listeners. I guess let's attach that to our _guestBookSubscription variable or field, I guess. Now one thing you might be wondering about is how come we're completely wiping out GuestBookMessages and recreating it from scratch every time we get a new snapshot? Like, if we were to add a new document, wouldn't just that new document get added to our listener? The answer here is no. Now, when a new document gets added to this collection, the fire stored database is generally smart enough to only send that new document up to your client. That's going to keep your data transfer smaller and faster. But what your client library will do is combine that new data with the cache data that it already has, and give you the whole thing together inside the SnapshotListener. The reason it does this is it makes your life a whole lot easier. You'll notice that this logic to recreate our GuestBookMessages list is really simple. We don't need any if-then logic around, well, if an object is added, do this, and if it's deleted, do this, and if it's changed do this, right? We just recreate the data in one fell swoop, and we don't need to worry about exactly how our data has changed. Now, there may be times when doing this work is really expensive. And if it is, Firestore can expose the details for you around what exactly has changed in our data. And I think they mentioned this in the notes here in the Codelab, but I would say the vast majority of the time, you don't really need to care. You can just recreate your data from scratch like we did here, and it will just work. And so then what we're going to do down here is, when you log out, we're going to wipe out those GuestBookMessages-- you will no longer get to see those-- and also cancel your guestbook subscription, if it exists. Again, this is basically good data hygiene. Any time you set up a listener in your app you want to remove or cancel it at the appropriate, corresponding time. So here we're adding our listener when our user signs in, so we should probably cancel it when our user signs out and we no longer need this data. So we now have our application state that is grabbing our GuestBookMessage and-- you know what-- let's just do a little print debugging here and show our document data in the console. Here, I'll add this in. And if I were to restart my application you can see that I'm now getting these messages down here in the console. And in fact, if I were to create another one, I would get back my list of messages again, including this new one that I just wrote, which is pretty neat. So I guess the only thing we need to do now is actually show that data in our app, and that's some fairly basic Flutter widget work. So I guess the first thing we'll do is change our GuestBook class so it not only has an addMessage function but also a list of Messages. And we'll add that to our fields here, and then let's see here-- down under GuestBookState they are wrapping this padding inside a column, so I'm just going to do it this way. It might be easier than cutting and pasting, and add our cross axis alignment of start-- and then they're adding in a little-sized box at the bottom, and then a bunch of paragraph widgets. This is one of those pre-styled widgets that they defined in widgets.dart for us, if you'll recall. And we will create one for each message. That will be followed by one more sized box. Finally, up here when we create GuestBook, we'll also need to pass in our GuestBookMessages list from our appState object. All right, so let's run this again. This works for me, although I am pretty sure there have been times in the past I had to actually stop and rerun this thing to get it to work properly. So try that if you're currently running into issues. But you should see our list of GuestBookMessages right there on our main screen. And if I were to go ahead and write another message here, you can see that when I hit Send this gets added nearly right away to our list of messages. And you know what? Let's do something even cooler. I'm going to open up the Firebase console, where I can see my list of messages in the database, and I'm going to go ahead and just change one of these. And when I hit update here, you can see that this change is reflected nearly right away in my app. And this works on the Name field, too. Let's change this to Joanne. And that name gets updated here in the app-- again-- nearly right away. I don't know about you, but I find this whole thing pretty magical. Maybe I'm just easily impressed. Anyway, one thing I'm going to do here that isn't in the Codelab, and you should probably get into the habit of doing this too-- this query up here? We don't have a limit. And I would like to add one. So I'm just going to grab the 10 most-- actually, you know what? Just so you can see what happens, the three most recent documents. And so if I were to restart this, you can see that it's now keeping only the three most recent messages. In fact, if I were to add a new message here you can see that it bumps one off of our list. You should get into the habit of doing this, or at least thinking about this if you're building applications where you're expecting to get thousands of documents in a collection. You might not want to read in every single one of these every time. Not only will that use up your user's data, but you're going to get charged for all those reads as well. So start thinking about how many documents your users are really going to care about seeing. Maybe I'll change this to 100, which I think should generally be fine. And of course, there are ways to paginate this data and get the next group of 100 if you want. You can always check out our documentation for more. OK, so it looks like we're able to read in all of our messages now. New ones appear pretty much instantly, and if you have the iOS and Android versions open at the same time, it's kind of fun to watch your messages appear back and forth between the two. So with that, we're getting pretty close to the end, but we haven't completed the most important step in building our app, and that's keeping it secure. So we'll see you in part eight. OK, here we are in part eight, where we're going to be setting up security rules. So one thing you might have noticed about writing data to Firestore is there's no intermediary service along the way, right? Like there's no server that the client needs to go through in order to process these requests. You don't really need to create any specific API endpoints or anything that would like add in some logic to make sure that a client is legit before pinging the database. In our situation, our client is essentially able to just talk to the database directly. And that is certainly cool in keeping your architecture simple, right? It takes an entire layer out of the way. But you do have to be careful here. We need to make sure that these clients that are talking directly to our database are only allowed to access the data that we want them to access, and that they're only able to make changes that you want them to make. And these are clients that are going to be out there in the real world in the hands of some potentially unscrupulous characters who might be able to modify your clients or modify these network requests that they make. So you can't really trust them. So we're going to do a little bit of locking down here to make sure our clients can only make the kind of database calls we want them to make. They probably won't be entirely complete, but they will be a good start and should at least get you into the habit of writing good security rules. I do recommend you check out the documentation or the videos when you want to do more of this for real. So let's bring up the Firebase console again, and here under the rules section right now you can see that anybody can read or write to anywhere in the database as long as that is happening sometime for the next 30 days from when I set this up, which is not ideal. First, I'm going to just remove this part here so now nobody can read or write to anywhere in our database. And so now I'm going to add in this match guestbook entry block, and what this is doing here is basically all of your Firestore security rules are going to start with this match databases documents line. But then within that I'm saying, OK, now let's take a look at any of our documents in our Guestbook collection. Now, this entry in curly brackets thing is basically a wild card match. As you might recall, inside my Guestbook collection I have a bunch of different documents with a bunch of different document IDs that are basically random strings. So this thing here is saying go ahead and match any document ID, no matter what it is, and you can store that document ID in a variable called, in this case, entry. Now in our situation we're going to ignore this variable. We don't need it here, but there are times when you will need this variable. In fact, I think we'll encounter one of those in the next section. So we'll uncover more about these wild cards then. So back to the set of rules. I guess the first thing we're going to do is say, you know, we'll allow people to read entries in this Guestbook as long as they are signed in. Now, this request.auth variable is kind of a special one. It's provided by the Firebase Auth library, and it contains some important information about the user, like their user ID, along with a signed token that our service can quickly verify server side, which means that we can trust it. So what we're going to do here is we're saying you can read anything from this collection only if you're signed in. If you're not signed in, you're not going to have a user ID. So let me check my app again. Looks like this is the case, I can read those documents still, which is good. So let me try writing a message and, look at that-- I got an error-- which we should, right? Because we no longer have permission to write to this collection. So far I've only given us read permission, so that write call has properly failed. I suppose I could say something like, well, we'll allow writes if our user is signed in as well. And then at that point anybody can read or write to our collection. But what they're doing here in the Codelab is a little more sophisticated. They're saying, well, you can write here if your request.auth.uid equals request.resource.data.userId. So what this is saying is the user ID of our user, as verified by Firebase Auth, equals-- and here, request.resource.data means let's take a look at the data of the document that you're trying to write. In our case, it's this document that we're trying to add here in the code where we've got our text and our timestamp and our name and our user ID. So we're saying this user ID corresponds to this variable here. And so what this rule is basically saying is, you can only write to a document if this user ID actually is your real user ID as verified by Firebase Auth. So with this rule our writes once again work. I can add something here in our Guestbook and it shows up. But if I went into the code here and made our user ID, you know, "fake fake fake"-- like let's say I'm some evildoer and I'm modifying our client code so that I can try and post Guestbook responses on behalf of another user-- like I'm saying here I'm actually user "fake fake fake", well now if I try to make a post on behalf of this user, I get an error. This request has been denied, which is what we want. So, yay-- working as intended. And so then down here they're saying, hey, you know what? As long as we're checking if user ID is correct, here's a bunch of other things you can do. Let's make sure that this document we're about to write also contains these other fields-- name, text, and timestamp. And so that's what this code does. But I'm going to say there's actually a slightly more sophisticated way of doing this. If I just say, request.resource .data.keys().hasAll, and then I pass in name, text, and timestamp as a list here, that's actually the same as these three lines below. This just may be a little more efficient and perhaps a little easier to read. So let's make sure this is working. I'll submit a normal post, and that works. And now let's leave off the name field and try this again. This is a post without a name, and if I send this off once again this gets properly rejected by our security rules. So we'll put this code back, submit one more post and-- OK, it looks like everything is working. So with that, we have made our app slightly more secure. We made sure that at the very least, you have to be signed in to view these messages. You cannot write a post with the user ID of somebody else. And we're making sure that all of our posts contain at least these three other fields. There's definitely a lot more you can do with security rules, and like I said at the beginning, we have lots of documentation and videos on the topic. So be sure to check those out. But for now, let's move on to the last chunk of work in our Codelab, which is adding in one more bonus feature. OK, so here we are in the last part of the Codelab-- the bonus step where we get to practice everything we've learned. So what we're going to do here is add in another feature to our Flutter app where, in addition to adding guestbook messages, users can RSVP for events by saying, yes, I plan on attending, or no, I'm not, and will also show the user how many folks are actually planning on attending. So maybe before we go through this, you might want to take a moment and ask yourself, well, how would I implement this based on everything we've learned so far? And maybe give it a try. Do a little git commit, open up a new branch and try implementing it on your own. I often find that the best way of learning things is to struggle on your own and make mistakes and try and figure it out before you see the actual solution. I think that can be a lot more effective when it comes to cementing your learning than just copying and pasting code that somebody else did. So go on, give it a try. Pause the video, try to implement this on your own, and then come on back and we will implement this together the way the Codelab authors did. All right, how'd you do? Well, let's see how the Codelab authors decided to write this. So on the database, in addition to our existing Guestbook collection, they've decided to create a collection called Attendees. And in there we're going to create a new document for each user, and that document is right now only going to contain one key-value pair. And that is the attending value, which is set to either true or false. So let's go ahead and do this-- it looks like first we're adding in a field with a getter here called attendees, and that's going to represent the number of people who have attended an event. I'm guessing we're putting this in our ApplicationState, so let's do that here. OK, next we're going to add in this field, called attending, that's going to represent whether or not the user has decided to attend the event. And let's see, when I copy this in it's giving me errors because this enum isn't defined anywhere. So I'm actually going to peek ahead a few steps and copy and paste this enum definition-- I guess let's put it here right after our definition of GuestBookMessage. And you can see that it has the values of yes, no, or unknown. So going back down to this field, it's private with the getter right down here. We're defining a StreamSubscription, which we will attach a listener to later on, but I think the thing that's interesting here is that our setter doesn't actually touch this value. What it does here is it creates this value directly to Firestore. That is, it creates a document in the attendees collection with a document ID that is equal to our signed in user's ID. That's this value that we're passing into our doc argument that's going to say create a document with this particular ID. And that's a pretty common thing to do whenever you want to store user specific data. And then we're setting the value of attending in our document to either true or false depending on this attending value that gets passed into our setter. Now all of this is fine, but we haven't actually affected the value of this private field yet. But don't worry, we will get to that shortly. So down here we start adding in a few real-time listeners, and this one here is pretty interesting. It's basically figuring out how many people are planning on attending this event. And how is it doing this? By querying all the documents in the attendees collection where the value of attending is true, and then it's counting the number of documents it receives. And I'll be honest here, I'm not super excited about the way we've implemented this. I mean, yes, it works and most importantly, it's very easy, which I think is sort of why they did it. But it might not be super efficient either from a data perspective or a cost perspective. For one thing, we're retrieving the content for all of this attendee data and yes, right now it only contains the single value. But what if this ends up becoming very large documents that contain a whole bunch of other user profile information or something. You'd be loading these in every time. And unless you're planning on displaying all that data of all the attendees on this page, you might end up gathering a bunch of information you don't really need just to get one single value. Also, from a cost perspective, you are getting charged for every individual document read and yeah, reading in 20 or 30 docs isn't going to be a big deal, even if you end up doing that multiple times. But if you had a few thousand documents that could start to add up. So if this were a real application, you might want to actually have a single attendees value that you store somewhere in a separate document, and then you could increment or decrement that value depending on whether or not somebody decides to attend. Granting, doing all that would make your code more complex, and as a general rule, I'm not a fan of overly optimizing for price at the risk of making your code too complex-- don't spend hundreds of dollars of engineering time to save $0.24 a month. So with all that said, I think this basic approach is fine for now. And if what I said just confused you, don't worry. We'll just stick with a simple version. But this might be a case where it could make sense to optimize our app in the future, if we expected to see some real traffic. But this is a pretty big digression. Let's just get back to our implementation here. So in addition to creating a listener to listen to all of our attendees documents, we're also creating a listener that's listening to the specific document that belongs to our signed in user. And it's setting this attending field in our code to either yes, no, or unknown depending on what the value in this document says or if this document even exists. And I do want to call this out because this is a pattern it took me a couple of times to wrap my head around when I first started using both the real-time database and then Firestore. You'll notice that when we set the value of attending we never directly set the value of this private field from within our setter. What we're doing is setting the value in Firestore. And then Firestore will go ahead and tell our listener, oh hey, this value has changed, and our listener can then set this internal variables value based on what it gets back from this document. And again, this is different than what I was used to when I first started working with Firebase. I was trying weird setups where I would set the value of this field and then use Firestore to back up this field to the Cloud. And that doesn't really work, right? The pattern you should really get used to here is seeing Firebase as your source of truth. That is, when you want to change or set a value, you should just set it in Firestore directly and let your real time listeners propagate those values to your internal fields or variables. Now, thanks to latency compensation and all the work our libraries are doing in your local cache, we are essentially able to reflect these changes right away, and you don't actually need to wait for these network calls to come back for your internal values to reflect your current source of truth. This will all still work even if your user has a slow internet connection or is offline. So again, look at how we're setting data here for this attendee field by using Firestore as a source of truth, and get used to this kind of pattern when you're working with data that's stored in Firestore. OK, so because we've set up this listener when we first log in our user we should make sure we do the right thing and properly cancel it when our user logs out, and that's what we're doing here. That reminds me-- should we also cancel this listener when we dispose of our ApplicationState? I think we probably should. I'm not sure this is going to make any practical difference here, but it's a good habit to get used to of just canceling any listener that you create. This will be just a little bit of extra credit work on our part. OK, so we've added the logic to set and retrieve whether or not a user is attending and to count how many people are planning on showing up to our event. Now all we need to do is show all of this information in our app. So this here is our widget that acts a little like a radio button or a switch. What we're passing in is an attending state and a callback that our widget should call when a user clicks on one of these buttons. And otherwise the logic is pretty straightforward, right? It's got two buttons, a yes and no button. And this widget changes the look of these buttons based on the value of this attending state. And our widget calls this onSelection callback function when the user clicks on these buttons. So let's just copy and paste this into the end of the document as one more class. And yes, we probably should be sticking these into separate files. If you want to go and do that I certainly wouldn't blame you. And then if we go back up to our main body here, we can copy over this bit of code to display the number of attendees. If you're logged in, not only will we display the Guestbook and discussion, but we can also show our yes no widgets so our user can decide if they want to attend. And if we were to go ahead and run this right now, we would get some errors because it turns out we don't have permission on the database level to set or read anything in our attendees collection, which is good, right? We should definitely be defaulting to not permitting things in our security model unless we explicitly add them. And that means our security rules are working as intended. But that doesn't make for very exciting demo or a useful product. So let's go ahead and update our security rules so that our app is able to perform these operations. I'm going to go back to our Firebase console, and I'm going to go ahead and add this block here. This says, for any document in our attendees collection-- well, anybody can read in our data. That's going to be publicly viewable-- but then this line here says that you're only allowed to write to a document if the verified user ID of our signed in user is equal to this user ID variable. And what is this variable? It's the ID of the document as captured here in this wild card-- this element here in curly brackets. And this is a pattern you will probably see a lot in security rules. We're creating a document with an ID that's equal to our signed in user and then only letting our user write or update or sometimes even read in that document, if and only if their user ID, as verified by Firebase Auth, is equal to the ID of this document. And since it looks like we want them to set this attending field we're also including this line here, which says, hey, your document has to at least have an attending value. Just a little bit of document validation. And so now if we go back and reload, I can now go ahead and change the value of the field. Doing so updates the value in Firestore, which in turn gets reflected right away in my app state, which in turn gets reflected in the look of these buttons as well as this text label telling me how many people are attending. And I can see this changing here too in the console, which is pretty neat. In fact, if I had two of these clients open, you can see how this gets reflected across both of these clients in real-time, which is super fun. So wow, with that I think we are done with the Codelab. We actually did a lot here. We got our user to sign in using a username and password that Firebase is storing for us, so we don't need to worry about storing passwords anywhere. We then learned how to write messages to Firestore and read in those messages in real-time, and display them on our screen in a list widget for a magical, real-timey experience. We also set up a few basic security rules so that our users couldn't do things we didn't want them to. And then we went and did the whole thing again for our RSVP section. Now, there's clearly a whole lot more that Firebase can do for you. It's got over a dozen different products. And we covered two of them. But you can do things like add Analytics or AB testing, add notifications, store large images using Cloud Storage, and a lot more. So go check out the documentation if you want to know more about what Firebase can do for you. Go check out this video series if you want to learn more about Firestore and NoSQL databases in general. And try adding in another feature or two to this Codelab. In fact, tell me in the comments below what you did. I'd be interested in hearing about it And thanks for learning along with me, everybody. See you soon. [MUSIC PLAYING]
Info
Channel: Firebase
Views: 131,739
Rating: undefined out of 5
Keywords: GDS: Yes, get to know firebase for flutter, firebase for flutter, firebase on flutter, flutter, flutter app, flutter application, emulator, device, android studio, firebase console, firebase codelab, firebase codelabs, flutter codelab, using firebase and flutter, using firebase on flutter, how to use flutter for firebase, flutter developers, developer, developers, firebase developers, google developers, firebase, google, Todd Kerpelman
Id: wUSkeTaBonA
Channel Id: undefined
Length: 57min 56sec (3476 seconds)
Published: Wed May 12 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.