[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]