19
Firebase Cloud Firestore
Written by Vincenzo Guzzi & Kevin D Moore
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
When you want to store information for hundreds of people, you can’t store it on one person’s phone. It has to be in a storage 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. This could take months of development time. 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 will create an instant messaging app called RayChat. While creating RayChat, 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 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 on an Android device. Don’t worry, you’ll run on iOS later.
You’ll see the RayChat home page:
Right now, your app doesn’t do much. You’ll need to add your own Cloud Firestore to send and receive messages.
What is a Cloud Firestore?
Google gives you the option for two real-time NoSQL document databases within the Firebase suite of tools: Realtime Database and Firebase Cloud Firestore. But what’s the difference?
Setting up a Google project and Database
Before you can use any of Google’s cloud services, you have to set up a project on the Firebase Console; then you can create your Cloud Firestore and manage it directly from the console. You’ll use the free tier.
Creating Google Services files
Google uses a config file that contains all of the API keys for your Firebase project. You’ll need to create a config file for your Android and iOS apps individually. You’ll start with Android.
Setting up Android
If you only see icons in the left margin, click on the arrow at the bottom of the list to expand the menu.
classpath 'com.google.gms:google-services:4.3.10'
apply plugin: 'com.google.gms.google-services'
Setting up iOS
You’ll need Xcode to set up your iOS project. If you’re not using a Mac, feel free to skip this section.
Adding Flutter dependencies
FlutterFire is the set of Flutter Firebase packages. You will use them to save and retrieve data. Go back to Android Studio and add the following dependencies in your pubspec.yaml. Add them underneath the intl
dependency:
firebase_core: ^1.6.0
cloud_firestore: ^2.5.3
Understanding Collections
Firestore stores data in collections, which are similar to tables in a traditional database. They have a name and a list of Documents.
Modeling data
Add a new directory inside lib called data. You’ll use this folder to store your data models and data access objects.
Adding a data model
Create a new file in the data directory called message.dart. Then add a new class with three fields, text
, date
and email
:
import 'package:cloud_firestore/cloud_firestore.dart';
class Message {
final String text;
final DateTime date;
final String? email;
DocumentReference? reference;
Message({
required this.text,
required this.date,
this.email,
this.reference
});
// TODO: Add JSON converters
}
factory Message.fromJson(Map<dynamic, dynamic> json) => Message(
text: json['text'] as String,
date: DateTime.parse(json['date'] as String),
email: json['email'] as String?);
Map<String, dynamic> toJson() => <String, dynamic>{
'date': date.toString(),
'text': text,
'email': email,
};
// TODO: Add fromSnapshot
factory Message.fromSnapshot(DocumentSnapshot snapshot) {
final message = Message.fromJson(snapshot.data() as Map<String, dynamic>);
message.reference = snapshot.reference;
return message;
}
Adding a data access object (DAO)
Create a new file in data called message_dao.dart. This is your DAO for your messages.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'message.dart';
class MessageDao {
// 1
final CollectionReference collection =
FirebaseFirestore.instance.collection('messages');
// TODO: Add saveMessage
}
void saveMessage(Message message) {
collection.add(message.toJson());
}
// TODO: Add getMessageStream
Stream<QuerySnapshot> getMessageStream() {
return collection.snapshots();
}
Provider
As you saw in Chapter 13, “State Management”, Provider is a great package for providing classes to its children. Open pubspec.yaml and add the provider
package:
provider: ^6.0.0
import 'package:firebase_core/firebase_core.dart';
import 'package:provider/provider.dart';
import '../data/message_dao.dart';
await Firebase.initializeApp();
return MultiProvider(
providers: [
// TODO: Add ChangeNotifierProvider<UserDao> here
Provider<MessageDao>(
lazy: false,
create: (_) => MessageDao(),
),
],
child:
),
Creating new messages
Open ui/message_list.dart and add Message
and MessageDao
as imports at the top of the file:
import 'package:provider/provider.dart';
import '../data/message.dart';
import '../data/message_dao.dart';
final messageDao = Provider.of<MessageDao>(context, listen: false);
_sendMessage(messageDao);
void _sendMessage(MessageDao messageDao) {
if (_canSendMessage()) {
final message = Message(
text: _messageController.text,
date: DateTime.now(),
// TODO: add email
);
messageDao.saveMessage(message);
_messageController.clear();
setState(() {});
}
}
Reactively displaying messages
Since MessageDao
has a getMessageStream()
method that returns a stream, you will use a StreamBuilder
to display messages.
import 'package:cloud_firestore/cloud_firestore.dart';
import 'message_widget.dart';
Widget _buildListItem(BuildContext context, DocumentSnapshot snapshot) {
// 1
final message = Message.fromSnapshot(snapshot);
// 2
return MessageWidget(
message.text,
message.date,
message.email
);
}
Widget _buildList(BuildContext context, List<DocumentSnapshot>? snapshot) {
// 1
return ListView(
controller: _scrollController,
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.only(top: 20.0),
// 2
children: snapshot!.map((data) => _buildListItem(context, data)).toList(),
);
}
Widget _getMessageList(MessageDao messageDao) {
return Expanded(
// 1
child: StreamBuilder<QuerySnapshot>(
// 2
stream: messageDao.getMessageStream(),
// 3
builder: (context, snapshot) {
// 4
if (!snapshot.hasData)
return const Center(child: LinearProgressIndicator());
// 5
return _buildList(context, snapshot.data!.docs);
},
),
);
}
_getMessageList(messageDao),
Authentication
Firebase provides user authorization and authentication with the FirebaseAuth
class, which allows you to:
Setting up Firebase Authentication
Return to the Firebase console. Click on the Authentication card and if prompted with Get started click on it, too:
Rules
Firebase database security consists of rules, which limit who can read and/or write to specific paths. The rules consist of a JSON string in the Rules tab.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
Firebase Authentication
To use authentication with Firebase, you will need the Firebase Authentication package. Add the following to the pubspec.yaml:
firebase_auth: ^3.1.1
User authentication
Just as you created a DAO for messages, you will create a DAO for users. In the data folder, create a new file named user_dao.dart and add the following imports:
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
class UserDao extends ChangeNotifier {
final auth = FirebaseAuth.instance;
// TODO: Add helper methods
}
// 1
bool isLoggedIn() {
return auth.currentUser != null;
}
// 2
String? userId() {
return auth.currentUser?.uid;
}
//3
String? email() {
return auth.currentUser?.email;
}
// TODO: Add signup
Signing up
The first task a user will need to perform is to create an account. Replace // TODO: Add signup
with:
// 1
void signup(String email, String password) async {
try {
// 2
await auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
// 3
notifyListeners();
} on FirebaseAuthException catch (e) {
// 4
if (e.code == 'weak-password') {
print('The password provided is too weak.');
} else if (e.code == 'email-already-in-use') {
print('The account already exists for that email.');
}
} catch (e) {
// 5
print(e);
}
}
// TODO: Add login
Logging in
Once a user has created an account, they can log back in. Replace // TODO: Add login
with:
// 1
void login(String email, String password) async {
try {
// 2
await auth.signInWithEmailAndPassword(
email: email,
password: password,
);
// 3
notifyListeners();
} on FirebaseAuthException catch (e) {
if (e.code == 'weak-password') {
print('The password provided is too weak.');
} else if (e.code == 'email-already-in-use') {
print('The account already exists for that email.');
}
} catch (e) {
print(e);
}
}
// TODO: Add logout
Logging out
The final feature is logout. Replace // TODO: Add logout
with:
void logout() async {
await auth.signOut();
notifyListeners();
}
Login screen
To get into the system, a user needs to log in. To do that, they need to create an account. You will be creating a dual-use login screen that will allow a user to either log in or sign up for a new account.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
import '../data/user_dao.dart';
class Login extends StatefulWidget {
const Login({Key? key}) : super(key: key);
@override
_LoginState createState() => _LoginState();
}
class _LoginState extends State<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
@override
Widget build(BuildContext context) {
// 1
final userDao = Provider.of<UserDao>(context, listen: false);
return Scaffold(
// 2
appBar: AppBar(
title: const Text('RayChat'),
),
body: Padding(
padding: const EdgeInsets.all(32.0),
// 3
child: Form(
key: _formKey,
// TODO: Add Column & Email
child: Column(
children: [
Row(
children: [
const SizedBox(height: 80),
Expanded(
// 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
Row(
children: [
const SizedBox(height: 20),
Expanded(
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
Row(
children: [
const SizedBox(height: 20),
Expanded(
child: ElevatedButton(
onPressed: () {
// 1
userDao.login(
_emailController.text, _passwordController.text);
},
child: const Text('Login'),
),
)
],
),
Row(
children: [
const SizedBox(height: 20),
Expanded(
child: ElevatedButton(
onPressed: () {
// 2
userDao.signup(
_emailController.text, _passwordController.text);
},
child: const Text('Sign Up'),
),
),
const SizedBox(height: 60),
],
),
// TODO: Add parentheses
],
),
),
),
);
}
}
import '../data/user_dao.dart';
import 'ui/login.dart';
ChangeNotifierProvider<UserDao>(
lazy: false,
create: (_) => UserDao(),
),
// 1
home: Consumer<UserDao>(
// 2
builder: (context, userDao, child) {
// 3
if (userDao.isLoggedIn()) {
return const MessageList();
} else {
return const Login();
}
},
),
Adding user handling code
First, return to message_list.dart and add this import:
import '../data/user_dao.dart';
String? email;
final userDao = Provider.of<UserDao>(context, listen: false);
email = userDao.email();
email: email,
Adding a logout button
Still in message_list.dart, replace // TODO: Replace with actions
with:
actions: [
IconButton(
onPressed: () {
userDao.logout();
},
icon: const Icon(Icons.logout),
),
],
Key points
- Cloud Firestore is a good solution for low-latency database storage.
- FlutterFire provides an easy way to use Firebase packages.
- Firebase provides authentication and security through Rules.
- Creating data access object (DAO) files helps to put Firebase functionalities in one place.
- 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, which can supercharge your app and give it enterprise-grade features. These include: