Chapters

Hide chapters

Flutter Apprentice

Second Edition · Flutter 2.5.1 · Dart 2.14.2 · Android Studio 2020.3.1

Section IV: Networking, Persistence and State

Section 4: 7 chapters
Show chapters Hide chapters

Appendices

Section 7: 2 chapters
Show chapters Hide chapters

19. Firebase Cloud Firestore
Written by Vincenzo Guzzi & Kevin D Moore

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now