16.
Firebase Cloud Firestore
Written by Stef Patterson & Kevin D Moore
When you want to store information for many people, you can’t realistically store it on one person’s phone. It has to be stored in the cloud. You could hire a team of developers to design and implement a backend system that connects to a database via a set of APIs. But, this could take months. Wouldn’t it be great if you could just connect to an existing system?
This is where Firebase Cloud Firestore comes in. You no longer need to write complicated apps that use thousands of lines of async tasks and threaded processes to simulate reactiveness. With Cloud Firestore, you’ll be up and running in no time.
In this chapter, you’ll add an instant messaging feature to the Yummy app.
While adding this feature, you’ll learn:
- About Cloud Firestore and when to use it.
- The steps required to set up a Firebase project with the Cloud Firestore.
- How to set up user authentication.
- How to connect to, query and populate the Cloud Firestore.
- How to use the Cloud Firestore to build your own instant messaging app.
Getting Started
First, open the starter project from this chapter’s project materials and run flutter pub get.
Next, build and run your project. You’ll see the Yummy app’s Chat tab.
Right now, your app doesn’t do much, but when you’re done, you’ll know how to use Cloud Firestore to send and receive messages.
What is Cloud Firestore?
Google has two NoSQL document databases within the Firebase suite of tools: Realtime Database and Cloud Firestore. But what’s the difference?
Google created Firestore to enable large-scale software with deeply layered data. You can query data and receive it separately, creating a truly elastic environment that copes well as your data set grows.
Realtime Database, though still a document-driven NoSQL database, returns data in JSON format. When you query a tree of JSON data, it includes all of its child nodes. To keep your transactions light and nimble, you have to keep your data hierarchy as flat as possible.
Both of these solutions are great and have similarities. They each have a free plan, and after you’ve reached your limit, you can pay-as-you-go. In both solutions, you don’t have to deploy and maintain your own servers, and each has live updates.
There are some differences, and it’s important to know when to use one and not the other. Here are some key areas for each database:
Firebase Cloud Firestore
- Has a free plan but charges per transaction and, to a lesser extent, for storage used past the limit.
- It’s easy to scale.
- Stores data in document collections.
- Can handle complex, deeply layered data sets and relations.
- Supports indexed queries with compound sorting and filtering.
- Available for mobile and web, including offline support.
Firebase Realtime Database
- Also has a free plan, but charges for storage used, not for queries made, past the limit.
- Extremely low latency.
- Data is stored in a single JSON tree.
- Easy to store simple data using JSON.
- You can either sort or filter on a query, but not both.
- Supports Apple and Android apps, including offline support. Doesn’t support offline web clients.
In this chapter, you’ll be using Cloud Firestore.
Note: To see a full comparison, see Google’s Choose a Database: Cloud Firestore or Realtime Database. Google has a lot of other database options beyond NoSQL databases.
Setting Up a Firebase Project
Before you can use any of Google’s Cloud services, you have to create a project on the Firebase Console.
Note: You’ll create your free tier Cloud Firestore database later.
First, go to https://console.firebase.google.com and click Create a project.
Name your project KodecoChat, and click Continue. If you’ve never created a Firebase project, you’ll be prompted to read and accept the Firebase terms, shown below on the left.
Disable Google Analytics since you don’t need it for this chapter, and click Create project.
Give Google a minute to create your project.
When your project’s ready, click Continue.
You should be returned to the Project Overview page.
Before you can add Firebase to your app, you need to have Firebase Command Line Interface (CLI) installed. You can skip the next section if you have it already installed. Leave your Firebase Project Overview open.
Installing Firebase CLI
What is Firebase CLI? It’s a Firebase management toolkit that enables running commands from command-line. Installation varies depending on your platform and preferred installation option — standalone binary or Node Package Manager (npm) that uses Node.js.
Google has great Firebase CLI reference documentation that will walk you through installation based on your computer’s operating system.
Once you’ve installed Firebase CLI, come back to continue adding Firebase to your app.
Using the Firebase CLI to Log In
To use Firebase from your IDE, you need to log in and select your project. Open Terminal and execute the following:
firebase login
This will ask you to allow Firebase to collect usage and error-reporting information.
If you don’t want to allow sharing, type n and press enter. Otherwise, press enter or accept — the default is Y.
Your browser should automatically open the Google login screen. Log in to the account you used to create your Firebase project.
After logging in, a consent message is displayed. Read the details, and assuming you agree, click Allow.
A login confirmation message is displayed. Close the tab/window.
Return to your Flutter IDE. In Terminal, you’ll see a success message.
Well done! You are all set. Now it’s time to add Firebase to your app.
Adding Firebase
The Firebase team has made things a lot easier for Flutter developers. You used to have to set up iOS, Android, and web apps separately. Now, you can add a Flutter app, and Firebase will do all the work for you.
Return to your Firebase Console in the browser. Tap the Flutter logo to add your app.
Tap Next since the Firebase CLI is ready to use and you already have a Flutter project.
As you can see, the next step includes command-line statements.
Don’t close your browser. Return to your Flutter IDE, and in Terminal, execute the following:
dart pub global activate flutterfire_cli
dart pub global gives you command-line access to the specified package from anywhere. You’ve just activated flutterfire_cli.
Make sure you’re at the root level of your Flutter project and run this, substituting the XXXXX for what your Firebase project reads.
flutterfire configure --project=kodecochat-XXXXX
Flutterfire connects to Firebase and lets you choose which platforms you wish to configure.
Use your arrow keys and spacebar if you wish to deselect a platform and press Enter. Wait a few minutes while your Firebase project is configured.
If there’s an update needed, as shown below, press Enter.
A message is displayed when the configuration is complete. It lists the Dart file it created as well as the Firebase App ID for each platform you selected.
Open lib/firebase_options.dart.
As you can see, Flutterfire CLI added all the code, including the new App ID details. Don’t edit this file, and ignore the red squiggles.
Close firebase_options.dart and return to your browser and the Firebase Console. Click Next.
Next, Google displays the code used for initializing your app, but you won’t be doing that step yet.
Click Continue to console. You’ll see that Firebase now has your Flutter apps listed. Refresh your browser if you don’t see them.
Tap the X apps button, where X is the number of platforms you chose when doing the Flutterfire Initialization, to see the automatically set up apps.
Awesome! Flutterfire has set up your Firebase project and has added code to your Flutter app. Now it’s time to add functionality to Yummy.
Open pubspec.yaml and after flutter_riverpod add the following, aligning each of these with flutter_riverpod:
firebase_auth: ^4.14.1
firebase_core: ^2.23.0
cloud_firestore: ^4.13.2
Run flutter pub get or click Pub get.
Open main.dart and look at the top of main(). You’ll notice WidgetsFlutterBinding.ensureInitialized(). Whenever you’re working with Firebase you need to have this to ensure there’s a platform channel to the device’s native code for Firebase initialization.
Replace // TODO: Add Firebase App Initialization with the following, ignoring any red squiggles:
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
Next, replace // TODO: Add Firebase core and options imports with:
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
initializeApp() initializes a Firebase instance and should be run before using any Flutterfire packages.
Note: If you receive an error message about multidex, this is because the Firebase package is so big. You need to enable multidex.
From Terminal, run
flutter run --debug, and when prompted, choose your Android device.When asked if you want to enable multidex support, enter
yand press Enter. Your app will continue to run.When you’re ready to continue with the chapter, return to Terminal and enter
qto stop your app.For additional details, see the Flutter docs on enabling multidex support.
Note: While your app will run on macOS, there are currently known macOS issues when using FlutterFire and Cloud Firestore. Depending on the situation, your app will run, but warnings will be printed.
Check the FlutterFire GitHub repo for issues. Some known issues are
trackingIDis deprecated and Cloud Firestore Xcode build times can take several minutes to render.
Run your app, and you’ll see that the UI hasn’t changed.
When using the chat feature, users want to keep their messages separate from other people’s. This means your app needs a way to keep track of each user and their messages. To do this, you need to add authentication to your app.
Adding Authentication
Firebase enables you to add user authentication without having to write and maintain your own server-side code. This can save you a lot of time and effort.
Firebase also gives you access to several different providers. For Yummy, you’re going to use email and password.
The FirebaseAuth class allows you to:
- Create a new user.
- Sign in a user.
- Sign out a user.
- Get data from that user.
Setting up Firebase Authentication
Return to the Firebase console in the browser. Click the Authentication card.
The next couple of steps can vary depending on if you’ve used Firebase before or not.
If prompted with another Authentication screen, click Get started. If not, go to the next step.
Next, if you see the Set up sign-in method button, click it. Otherwise, go to the next step.
When you see the Authentication section, click Add new provider.
As mentioned before, you’re going to be using Email/Password for Yummy, but before you proceed, take a look at all the different authentication options available.
When you’re ready, under Native providers, choose Email/Password.
Click the Email/Password Enable switch, leaving the email link disabled and click Save.
You’ve now enabled authentication. It’s time to talk about how Firebase stores data.
Understanding Firestore Data Storage
Cloud Firestore stores data in Documents that are like JSON dictionaries in key/value pairs. These pairs are called fields. Documents can also contain nested subcollections and arrays.
Fields can have several different types:
- String
- Number
- Boolean
- Map
- Array
- Null
- Timestamp
- Geopoint
- Reference to another document
This is a very basic document example:
{
"name": "Jane Doe",
"department": 250,
"occupation": "Flutter Developer"
}
This document has three fields: name, department and occupation. There are two string fields and one number.
A collection of documents is called… wait for it… Collections. Collections can only store 1 MB Documents.
[
{
"name": "Jane Doe",
"department": 250,
"occupation": "Flutter Developer"
},
{
"name": "John Doe",
"department": 500,
"occupation": "Flutter Developer"
}
]
This collection contains two documents.
You can use Firestore’s console to manually enter data and see the data appear almost immediately in your app. If you enter data in your app, you’ll see it appear on the web and other apps just as fast.
Now that you know about collections, are you ready to create your app’s database? Thought so. :]
Creating Cloud Firestore Database
Return to the Firebase Project Overview page.
Tap Cloud Firestore. If you don’t see it tap See all Build features.
Select Create Database.
Next, select your region for your Location and then click Next:
Select Start in test mode and click Enable.
This ensures you can read and write data easily while developing your app.
You’ll see steps displayed while your database is being created. It can go fast, so don’t be worried if you don’t see the same text.
After your database has been created, you’ll be redirected to your database console.
You can come back to the Data page later to see your app data in real time.
By default, Firestore is set up so that anyone can write to your database if they have the connection details. You don’t want that, do you? Next, you’ll set up database security and rules for limiting access.
Firebase Security Rules
Firebase database security consists of rules that limit who can read and/or write to specific paths. The rules consist of a JSON string in the Rules tab.
When you set up the database, you used the test ruleset. You need to lock down the database so that only those who have logged into your app can read and write messages.
From the Cloud Firestore Database screen, select the Rules tab.
Replace the current rules with:
// 1
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
// 2
allow read, write: if request.auth != null;
}
}
}
- Rules version 2 changed recursive wildcard behavior and is required when using collections. For more details, see the Cloud Firestore Security documentation
-
authis a special variable and contains the current user information. By checking to make sure that it’s notnull, you ensure a user is logged in.
When you’re ready, click Publish to save the changes.
Now that your database is set up and security is in place, it’s time to connect your app to your new Firebase project.
Modeling Data
Data modeling is an important part of your app development process. By creating a data model, you can ensure that the data is organized and stored in a way that is efficient, scalable and secure.
To keep your data models separate from your UI, you’ll use the lib/models folder to store your data models and data access objects (DAO).
Creating User Data Access Object (DAO)
In lib/models, create a new file named user_dao.dart and add the following:
import 'dart:developer';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
// 1
class UserDao extends ChangeNotifier {
String errorMsg = 'An error has occurred.';
// 2
final auth = FirebaseAuth.instance;
// TODO: Add helper methods
}
Here are a few things to highlight in the code above:
- The
UserDaoclass extendsChangeNotifierso you can notify any listeners whenever a user has logged in or logged out. - The
authvariable is used to hold on to an instance ofFirebaseAuth.
Next, replace // TODO: Add helper methods with:
// 1
bool isLoggedIn() {
return auth.currentUser != null;
}
// 2
String? userId() {
return auth.currentUser?.uid;
}
//3
String? email() {
return auth.currentUser?.email;
}
// TODO: Add signup
In this code, you:
- Return
trueif the user is logged in. If the current user isnull, they’re logged out. - Return the ID of the current user, which could be
null. - Return the email of the current user.
Signing Up
The first task for a user is to create an account. Replace // TODO: Add signup with:
// 1
Future<String?> signup(String email, String password) async {
try {
// 2
await auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
// 3
notifyListeners();
return null;
} on FirebaseAuthException catch (e) {
// 4
if (email.isEmpty) {
errorMsg = 'Email is blank.';
} else if (password.isEmpty) {
errorMsg = 'Password is blank.';
} else if (e.code == 'weak-password') {
errorMsg = 'The password provided is too weak.';
} else if (e.code == 'email-already-in-use') {
errorMsg = 'The account already exists for that email.';
}
return errorMsg;
} catch (e) {
// 5
log(e.toString());
return e.toString();
}
}
// TODO: Add login
Here you:
- Pass in the email and password the user entered. For a real app, you’ll need to make sure those
Stringsmeet your requirements. Return an error message if needed. - Call the Firebase method, which creates a new account with email and password.
- Notify all listeners so they can then check when a user is logged in.
- Handle some common errors.
- Catch any other type of exception.
Logging In
Once a user has created an account, they can log in. Replace // TODO: Add login with:
// 1
Future<String?> login(String email, String password) async {
try {
// 2
await auth.signInWithEmailAndPassword(
email: email,
password: password,
);
// 3
notifyListeners();
return null;
} on FirebaseAuthException catch (e) {
// 4
if (email.isEmpty) {
errorMsg = 'Email is blank.';
} else if (password.isEmpty) {
errorMsg = 'Password is blank.';
} else if (e.code == 'invalid-email') {
errorMsg = 'Invalid email.';
} else if (e.code == 'INVALID_LOGIN_CREDENTIALS') {
errorMsg = 'Invalid credentials.';
} else if (e.code == 'user-not-found') {
errorMsg = 'No user found for that email.';
} else if (e.code == 'wrong-password') {
errorMsg = 'Wrong password provided for that user.';
}
return errorMsg;
} catch (e) {
// 5
log(e.toString());
return e.toString();
}
}
// TODO: Add logout
Here, you:
- Pass in the email and password the user entered. Return an error message if needed.
- Call the Firebase method to log in to their account.
- Notify all listeners.
- Handle some common errors.
- Catch any other type of exception.
Logging Out
The final feature is log out. Replace // TODO: Add logout with:
void logout() async {
await auth.signOut();
notifyListeners();
}
Now that all the logic is in place, you’ll build the UI to log in.
Adopting Riverpod
As you saw in Chapter 13, “Managing State”, Riverpod is a great package for providing classes to its children. Your screens need access to these DAO classes. To do that, you’ll create two providers: one for user data and the other for messages.
Create a new file, providers.dart, in the lib directory and add the following:
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'models/user_dao.dart';
// 1
final userDaoProvider = ChangeNotifierProvider<UserDao>((ref) {
return UserDao();
});
// TODO: Add messageDaoProvider
// TODO: Add messageListProvider
-
UserDaoextendsChangeNotifier; useChangeNotifierProviderto provide an instance ofUserDao.
Next, you’ll create a login screen.
Creating the Login Screen
To use your app, a user needs to log in. To do that, they need to create an account. You’ll create a dual-use login screen that will allow a user to either log in or sign up for a new account.
In the components folder, create a new file called login.dart. Add the following, ignoring the red squiggles for now:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers.dart';
class Login extends ConsumerStatefulWidget {
const Login({
super.key,
});
@override
ConsumerState createState() => _LoginState();
}
class _LoginState extends ConsumerState<Login> {
// 1
final _emailController = TextEditingController();
// 2
final _passwordController = TextEditingController();
// 3
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
@override
void dispose() {
// 4
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
// TODO: Add build
Here, you:
- Create a text controller for the email field.
- Create a text controller for the password field.
- Create a key needed for a form.
- Dispose of the editing controllers.
Now, you’ll add the UI. Still ignoring the red squiggles, replace // TODO: Add build with:
@override
Widget build(BuildContext context) {
// 1
final userDao = ref.watch(userDaoProvider);
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(32.0),
// 2
child: Form(
key: _formKey,
// TODO: Add Column & Email
In this code, you:
- Use the Riverpod’s ref to watch the changes that take place in
UserDao. - Create the
Formwith the global key.
Next, you’ll create a column with four rows for email field, password field, login button, and a signup button.
Replace // TODO: Add Column & Email with:
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
// 1
child: TextFormField(
decoration: const InputDecoration(
border: UnderlineInputBorder(),
hintText: 'Email Address',
),
autofocus: false,
// 2
keyboardType: TextInputType.emailAddress,
// 3
textCapitalization: TextCapitalization.none,
autocorrect: false,
// 4
controller: _emailController,
// 5
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Email Required';
}
return null;
},
),
),
// TODO: Add Password
Here, you:
- Create the field for the email address.
- Use an email address keyboard type.
- Turn off auto-correction and capitalization.
- Set the editing controller.
- Define a validator to check for empty strings. You can use regular expressions or any other type of validation if you like.
Next, add the password field. Replace // TODO: Add Password with:
Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: TextFormField(
decoration: const InputDecoration(
border: UnderlineInputBorder(),
hintText: 'Password',
),
autofocus: false,
obscureText: true,
keyboardType: TextInputType.visiblePassword,
textCapitalization: TextCapitalization.none,
autocorrect: false,
controller: _passwordController,
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Password Required';
}
return null;
},
),
),
const Spacer(),
// TODO: Add Buttons
This is almost the same as the email field except for the added password field.
Now replace // TODO: Add Buttons with:
SizedBox(
width: double.infinity,
child: ElevatedButton(
// 1
onPressed: () async {
if (_formKey.currentState!.validate()) {
final errorMessage = await userDao.login(
_emailController.text,
_passwordController.text,
);
// 2
if (errorMessage != null) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage),
duration: const Duration(milliseconds: 700),
),
);
}
}
},
child: const Text('Login'),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
// 3
onPressed: () async {
if (_formKey.currentState!.validate()) {
final errorMessage = await userDao.signup(
_emailController.text,
_passwordController.text,
);
if (errorMessage != null) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(errorMessage),
duration: const Duration(milliseconds: 700),
),
);
}
}
},
child: const Text('Sign Up'),
),
),
),
// TODO: Add parentheses
Here, you:
- Set the first button to call the
login()method and show any error messages. - If there’s an error message, first check to see if the state object is “mounted” (still showing), then show a snackbar.
- Set the second button to call the
signup()method and show any error messages.
Now, replace // TODO: Add parentheses with:
],
),
),
),
);
}
}
Reformat the code to clean things up. You now have a screen that accepts an email address and password, and can log in or sign up a user.
Open home.dart, and in the build method, replace // TODO: Add userDaoProvider with the following, ignoring the red squiggles:
final userDao = ref.watch(userDaoProvider);
Using the watch method, any time the user state changes, you’ll either show the login screen or the message screen.
find // TODO: Add Login and replace the below code with:
Center(
child: userDao.isLoggedIn()
? const MessageList()
: const Login(),
),
If the user is logged in, then MessageList is shown. Otherwise Login is shown.
Add the following imports at the top:
import '../components/login.dart';
import 'providers.dart';
Stop and restart your app. You should then see the new login screen. Enter an email and a password. Remember the password :).
Note: Use at least six characters for the password.
Click Login.
An error is displayed because the user hasn’t signed up yet. Try again, but this time click Sign up.
Back in your browser, check the Firebase Authentication panel on the Users tab. You should see the added email address(es):
The user can log in, but how do they log out? Next, you’ll add a logout button.
Adding a Logout Button
Still in home.dart, replace // TODO: Replace with logout button with:
IconButton(
onPressed: () {
userDao.logout();
},
icon: const Icon(Icons.logout),
),
This will add the logout icon to AppBar and call logout() on the instance of UserDao.
Hot reload the app, and you’ll see the Messages screen. Then click the Logout button:
This will take you back to the login screen. Enter your email and password, and this time, click Login. You’ll be logged back in.
It’s time to display the messages list in the correct order and with the correct user details.
Adding Message Data Model
Create a new file in the lib/components directory called message.dart. Then, add the following class with date, email, text and reference properties:
import 'package:cloud_firestore/cloud_firestore.dart';
class Message {
Message({
required this.date,
required this.email,
required this.text,
this.reference,
});
final DateTime date;
final String email;
final String text;
DocumentReference? reference;
// TODO: Add JSON converters
}
You also need a way to transform your Message model from JSON since that’s how it’s stored in your Cloud Firestore. Replace // TODO: Add JSON converters with:
// 1
factory Message.fromJson(Map<dynamic, dynamic> json) => Message(
date: (json['date'] as Timestamp).toDate(),
email: json['email'] as String,
text: json['text'] as String,
);
// 2
Map<String, dynamic> toJson() => <String, dynamic>{
'date': date,
'email': email,
'text': text,
};
// TODO: Add fromSnapshot
- This transforms the JSON received from Cloud Firestore into a
Message. - This does the opposite — transforms the
Messageinto JSON for saving.
Replace // TODO: Add fromSnapshot with:
factory Message.fromSnapshot(DocumentSnapshot snapshot) {
// 1
final message = Message.fromJson(
snapshot.data() as Map<String, dynamic>,
);
// 2
message.reference = snapshot.reference;
return message;
}
- This takes a Firestore snapshot and converts it to a message using
fromJson(). - Sets the
referenceproperty.
Next, you’ll set up the message DAO.
Adding Message DAO
Create a new file in lib/models called message_dao.dart. This is your DAO for your messages.
Add the following:
import 'package:cloud_firestore/cloud_firestore.dart';
import '../components/message.dart';
import 'user_dao.dart';
class MessageDao {
MessageDao(this.userDao);
final UserDao userDao;
// 1
final CollectionReference collection =
FirebaseFirestore.instance.collection('messages');
// TODO: Add saveMessage
}
This code:
- Gets an instance of
FirebaseFirestoreand then gets the root of the messages collection by callingcollection().
Now, you need MessageDao to perform two functions: saving and retrieving.
Replace // TODO: Add saveMessage with:
void sendMessage(String text) {
// 1
final message = Message(
date: DateTime.now(),
email: userDao.email()!,
text: text,
);
// 3
collection.add(message.toJson()); // 2
}
// TODO: Add getMessageStream
This function:
- Creates a
Messageobject using the currentDateTime, the usersemailand their message as text. -
toJson()converts the message to a JSON string. -
add()Adds the string to the collection. This updates the database immediately.
For the retrieval method, you only need to expose a Stream<QuerySnapshot>, which interacts directly with your DatabaseReference.
Replace // TODO: Add getMessageStream with:
Stream<List<Message>> getMessageStream() {
return collection
.orderBy('date', descending: true)
.snapshots()
.map((snapshot) {
return [...snapshot.docs.map(Message.fromSnapshot)];
});
}
This returns a stream of data at the root level, ordering the collection by the date in descending order.
Now you have your message DAO. As the name states, the data access object helps you access whatever data you have stored at the given Cloud Firestore reference. It will also let you store new data as you send messages.
Open lib/providers.dart, and, again ignoring red squiggles, replace // TODO: Add messageDaoProvider with the following:
final messageDaoProvider = Provider<MessageDao>((ref) {
return MessageDao(ref.watch(userDaoProvider));
});
This returns MessageDao. Now, all you have to do is build your UI.
Replace // TODO: Add messageListProvider with the following:
final messageListProvider = StreamProvider<List<Message>>((ref) {
final messageDao = ref.watch(messageDaoProvider);
return messageDao.getMessageStream();
});
Here you’ve used StreamProvider to get a stream of messages from the MessageDao.
Add the following to the top.
import 'components/message.dart';
import 'models/message_dao.dart';
Next, you’ll use these providers to build your message list UI.
Creating New Messages
Open components/message_list.dart. Replace // TODO: Replace _sendMessage and the line beneath it with your new send message code:
void _sendMessage() {
if (_messageController.text.isNotEmpty) {
// 1
final messageDao = ref.read(messageDaoProvider);
// 2
messageDao.sendMessage(_messageController.text.trim());
_messageController.clear();
}
}
Here you’re using:
-
ref.read()to use theMessageDao -
trim()to then send the message to remove leading and trailing blanks.
Add your new providers import at the top of the file:
import '../providers.dart';
Stop the app and re-run it on one device. You’ll see the same screen as you did before. Type your first message and click the → button.
Now, go back to your Firebase Console and open your project’s Cloud Firestore. You’ll see your message as an entry:
Great job! You’ve implemented a remote database and added an entry with very little code.
Note: All of the blurred random letters will be different for each person.
Try adding a few more messages. You can even watch your Cloud Firestore as you enter each message to see them appear in real time.
Now, it’s time to display those messages.
Reactively Displaying Messages
Now that you have a stream of messages, you want to display them.
Open lib/components/message_widget.dart.
Find // TODO: Replace MessageWidget and replace the MessageWidget with the below code, ignoring those pesky red squiggles:
const MessageWidget(
this.message, {
super.key,
});
final Message message;
Here, you’ve added a message object to the MessageWidget constructor.
Find // TODO: Add userDao and myMessage and replace it with:
// 1
final userDao = ref.watch(userDaoProvider);
//2
final myMessage = message.email == userDao.email();
This code:
- Uses
ref.watch()to listen to the changes inUserDao. - Checks if the message’s email is the same as the user’s email.
At the top of the file, add the following import:
import 'package:intl/intl.dart';
import '../providers.dart';
import 'message.dart';
Display the message text by replacing // TODO: Replace Text, and the line under it with:
Text(
message.text,
style: theme.textTheme.bodyLarge!,
),
Find // TODO: Remove const, and remove the const from the child beneath it.
Next, you need to add a row to display the messages as they come in. Locate // TODO: Add Row for message and replace it with:
Row(
// TODO: Add mainAxisAlignment
children: [
// Display email of others not ones sent from device
!myMessage
? Text(
message.email,
style: TextStyle(
color: theme.colorScheme.secondary,
),
)
// If message is sent from the device display nothing
: const Text(''),
// Display date and time message was sent
Text(
' ${DateFormat.yMd().format(message.date)} '
'${DateFormat.Hm().format(message.date)}',
style: TextStyle(
color: theme.colorScheme.secondary,
),
),
],
),
Here, you’re displaying the message sender’s email address if it’s not from the device and the date and time it was sent.
To prevent the messages from taking up the whole width of the device. Find // TODO: Add crossAxisAlignment and replace it with the following:
crossAxisAlignment: myMessage //
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
If the message is from the device, then the speech bubble will be on the right. Otherwise, it’s on the left.
Right now, the messages would align in the middle of the screen. Find and replace // TODO: ADD alignment in FractionallySizedBox.
alignment: myMessage //
? Alignment.topRight
: Alignment.topLeft,
To have the email and date/time aligned with their speech bubble, replace // TODO: Add mainAxisAlignment with:
mainAxisAlignment: myMessage //
? MainAxisAlignment.end
: MainAxisAlignment.start,
If the message is from the device, then it’ll be on the right. Otherwise, it’s on the left.
Since MessageDao has a getMessageStream() method that returns a stream, you’ll use a StreamBuilder to display messages.
Back in message_list.dart, find // TODO: Add Message List and replace it and the whole Expanded widget with the following:
Expanded(
// 1
child: Consumer(
builder: (BuildContext context, WidgetRef ref, Widget? child) {
final data = ref.watch(messageListProvider);
return data.when(
loading: () => const Center(
child: LinearProgressIndicator(),
),
data: (List<Message> messages) => ListView(
controller: _scrollController,
reverse: true,
// 2
children: [
for (final message in messages) //
Padding(
padding:
const EdgeInsets.fromLTRB(24.0, 12.0, 24.0, 4.0),
child: MessageWidget(message),
),
],
),
error: (error, stackTrace) {
return Center(child: Text('$error'));
},
);
},
),
),
Here you:
- Create a new message from the given snapshot.
- Pass the message info to the MessageWidget.
Add the following imports:
import 'message.dart';
import 'message_widget.dart';
Trigger a hot reload, and you’ll see your messages in a list.
Load your app on multiple devices or simulators and watch as you communicate in real time and see the messages appear on them simultaneously.
Magic!
Notice how the messages are labeled with the email of the user who sent them, except on the device that sent the message. In that case, only the time is shown.
You now have a fully working chat app that can be used by multiple people. Great job!
Key Points
- Cloud Firestore is a good solution for low-latency database storage.
- FlutterFire provides an easy way to use Firebase packages.
- Firebase provides serverless authentication and security through Rules.
- Creating data access object (DAO) files helps to put Firebase functionalities in one place.
- Use Firestore to store and retrieve data in real time.
- You can choose many different types of authentication, from email to other services.
Where to Go From Here?
There are plenty of other Cloud Firestore features that can supercharge your app and give it enterprise-grade features. These include:
- Offline capabilities: Keep your data in sync even when offline. here: https://firebase.google.com/docs/firestore/manage-data/enable-offline.
- Database Rules: Make your database more secure, here: https://firebase.google.com/docs/database/security.
- More sign-up methods: Use similar features to Google and Apple sign-in.
There are plenty of other great Firebase products you can integrate with. Check out the rest of the Firebase API here: https://firebase.flutter.dev/docs/overview/#next-steps.