Firebase User Authentication in Flutter

In our last tutorial, we used Firebase to store and keep track of the tasks we needed to complete. But that approach wasn’t customized to a specific user, i.e. they all got the same list of tasks. One way of personalizing these tasks for different users is to include user authentication. And that’s exactly what we’ll be doing in this post.

In this post we will:

  1. Allow user authentication from Firebase
  2. Create register, login, and splash pages
  3. Create a user object during the registration process
  4. Handle a collection of tasks for each user

Step 1 — Enable Firebase Authentication

Go to the Authentication option inside the Develop section of the Firebase project dashboard.

As you can see, all methods of authentications are, by default, disabled. For now, enable Email/Password so we can begin using it to register an account. Since we’re enabling only this provider, we can signup and login with an email and password only.

Under templates, we find templates for common emails. If you wish, you can change a few of the details, like the project name and the name of the sender.

Step 2 — Integrate Firebase Authentication

If you’re continuing from the last post, you’ll only need to install a FlutterFire plugin. If not, please take a look at the blog post shared above to integrate Firebase into the app.

FlutterFire is a collection of plugins for each Firebase service. FlutterFire can be used by both platforms, as it depends on the multi-platform Flutter SDK.

Find the different plugins for each service here:

For authentication, I’ll need to install firebase_auth, the official FlutterFire plugin.

Update the pubspec.yaml file to include the plugin dependency below, inside dependencies:

Run flutter packages get once more to install the required packages.

To use this package, we need to import it, as shown below:

Step 3— Change the Navigation Flow

To include user authentication, we’ll need to change the flow the user takes through the app. One of the most common methods is to check the authentication status of the user in a Splash page. Then, depending on the status, we direct the user to the authentication flow (Login) or the main app flow (Home).

To implement the flow described above, we’ll need to update the main.dart file to include the following code:

In the above code, the named routes correspond to a specific page. We’ll need to make the following Widgets;

  1. SplashPage
  2. LoginPage
  3. RegisterPage

SplashPage

import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'home.dart';

class SplashPage extends StatefulWidget {
  SplashPage({Key key}) : super(key: key);

  @override
  _SplashPageState createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> {
  @override
  initState() {
    FirebaseAuth.instance
        .currentUser()
        .then((currentUser) => {
              if (currentUser == null)
                {Navigator.pushReplacementNamed(context, "/login")}
              else
                {
                  Firestore.instance
                      .collection("users")
                      .document(currentUser.uid)
                      .get()
                      .then((DocumentSnapshot result) =>
                          Navigator.pushReplacement(
                              context,
                              MaterialPageRoute(
                                  builder: (context) => HomePage(
                                        title: result["fname"] + "'s Tasks",
                                        uid: currentUser.uid,
                                      ))))
                      .catchError((err) => print(err))
                }
            })
        .catchError((err) => print(err));
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Container(
          child: Text("Loading..."),
        ),
      ),
    );
  }
}

In the above page we:

  1. Make a request to the FirebaseAuth instance, to see if the user is already logged in.
  2. The currentUser method returns the logged-in user’s information, like their unique id (uid).
  3. If the currentUser object isn’t found, the user is directed to the Login page.
  4. We use the Navigator method pushReplacement to make the Login or Home page—the first in the stack. This is so the user does not navigate back to the Splash page.
  5. If a user is already logged in, we get the user object from the Cloud Firestore service. This contains all data and tasks related to that particular user. (More on this in the Register page.)
  6. We use the first name of the user in the title of the Home page to further personalize the user’s experience.
  7. For now, the UI for the Splash page is a simple Text Widget, displaying “Loading…”.

RegisterPage

import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:my_first_app/home.dart';

class RegisterPage extends StatefulWidget {
  RegisterPage({Key key}) : super(key: key);

  @override
  _RegisterPageState createState() => _RegisterPageState();
}

class _RegisterPageState extends State<RegisterPage> {
  final GlobalKey<FormState> _registerFormKey = GlobalKey<FormState>();
  TextEditingController firstNameInputController;
  TextEditingController lastNameInputController;
  TextEditingController emailInputController;
  TextEditingController pwdInputController;
  TextEditingController confirmPwdInputController;

  @override
  initState() {
    firstNameInputController = new TextEditingController();
    lastNameInputController = new TextEditingController();
    emailInputController = new TextEditingController();
    pwdInputController = new TextEditingController();
    confirmPwdInputController = new TextEditingController();
    super.initState();
  }

  String emailValidator(String value) {
    Pattern pattern =
        r'^(([^<>()[]\.,;:s@"]+(.[^<>()[]\.,;:s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$';
    RegExp regex = new RegExp(pattern);
    if (!regex.hasMatch(value)) {
      return 'Email format is invalid';
    } else {
      return null;
    }
  }

  String pwdValidator(String value) {
    if (value.length < 8) {
      return 'Password must be longer than 8 characters';
    } else {
      return null;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Register"),
        ),
        body: Container(
            padding: const EdgeInsets.all(20.0),
            child: SingleChildScrollView(
                child: Form(
              key: _registerFormKey,
              child: Column(
                children: <Widget>[
                  TextFormField(
                    decoration: InputDecoration(
                        labelText: 'First Name*', hintText: "John"),
                    controller: firstNameInputController,
                    validator: (value) {
                      if (value.length < 3) {
                        return "Please enter a valid first name.";
                      }
                    },
                  ),
                  TextFormField(
                      decoration: InputDecoration(
                          labelText: 'Last Name*', hintText: "Doe"),
                      controller: lastNameInputController,
                      validator: (value) {
                        if (value.length < 3) {
                          return "Please enter a valid last name.";
                        }
                      }),
                  TextFormField(
                    decoration: InputDecoration(
                        labelText: 'Email*', hintText: "[email protected]"),
                    controller: emailInputController,
                    keyboardType: TextInputType.emailAddress,
                    validator: emailValidator,
                  ),
                  TextFormField(
                    decoration: InputDecoration(
                        labelText: 'Password*', hintText: "********"),
                    controller: pwdInputController,
                    obscureText: true,
                    validator: pwdValidator,
                  ),
                  TextFormField(
                    decoration: InputDecoration(
                        labelText: 'Confirm Password*', hintText: "********"),
                    controller: confirmPwdInputController,
                    obscureText: true,
                    validator: pwdValidator,
                  ),
                  RaisedButton(
                    child: Text("Register"),
                    color: Theme.of(context).primaryColor,
                    textColor: Colors.white,
                    onPressed: () {
                      if (_registerFormKey.currentState.validate()) {
                        if (pwdInputController.text ==
                            confirmPwdInputController.text) {
                          FirebaseAuth.instance
                              .createUserWithEmailAndPassword(
                                  email: emailInputController.text,
                                  password: pwdInputController.text)
                              .then((currentUser) => Firestore.instance
                                  .collection("users")
                                  .document(currentUser.uid)
                                  .setData({
                                    "uid": currentUser.uid,
                                    "fname": firstNameInputController.text,
                                    "surname": lastNameInputController.text,
                                    "email": emailInputController.text,
                                  })
                                  .then((result) => {
                                        Navigator.pushAndRemoveUntil(
                                            context,
                                            MaterialPageRoute(
                                                builder: (context) => HomePage(
                                                      title:
                                                          firstNameInputController
                                                                  .text +
                                                              "'s Tasks",
                                                      uid: currentUser.uid,
                                                    )),
                                            (_) => false),
                                        firstNameInputController.clear(),
                                        lastNameInputController.clear(),
                                        emailInputController.clear(),
                                        pwdInputController.clear(),
                                        confirmPwdInputController.clear()
                                      })
                                  .catchError((err) => print(err)))
                              .catchError((err) => print(err));
                        } else {
                          showDialog(
                              context: context,
                              builder: (BuildContext context) {
                                return AlertDialog(
                                  title: Text("Error"),
                                  content: Text("The passwords do not match"),
                                  actions: <Widget>[
                                    FlatButton(
                                      child: Text("Close"),
                                      onPressed: () {
                                        Navigator.of(context).pop();
                                      },
                                    )
                                  ],
                                );
                              });
                        }
                      }
                    },
                  ),
                  Text("Already have an account?"),
                  FlatButton(
                    child: Text("Login here!"),
                    onPressed: () {
                      Navigator.pop(context);
                    },
                  )
                ],
              ),
            ))));
  }
}
  1. The UI for the Register page is displayed to the left. It consists of 5 text fields to capture the user’s first and last name, email, password, and confirmed password. It also includes one RaisedButton Widget to register the user. A FlatButton Widget is included to redirect the user to the login page if they already have an account.
  2. Each TextFormField Widget has its own controller to handle and read the data entered into it.
  3. Each TextFormField Widget has its own validator.
  4. Both names are validated to see if they are more than 2 letters.
  5. The email is checked by an email regex pattern.
  6. The password fields are checked if their length is more than 7 characters. The fields also have the tag obscureText made true, to hide the password values.
  7. The form has a unique GlobalKey. This is to check the current state of the form while registering, to see if all fields pass their validations. If any field fails the validation, an error message is displayed below the text field.
  8. When the Register button is pressed, the form fields are first checked if they pass their validations. The next step is to check if both the password fields match. If they don’t, an alert is displayed to the user to inform them of the error.
  9. If they do match, the method createUserWithEmailAndPassword of the FirebaseAuth instance is used to signup a new user.
  10. If the user is successfully signed up, a new document is created containing their first and last names, email, and uid (the same mentioned above in the Splash page). To make it easier to get this new object from Cloud Firestore, the document ID is the user’s uid.
  11. If both Firebase actions are a success, the user is directed to the Home page. The Home page includes the user’s first name in the title.
  12. The Navigator method pushAndRemoveUntil is used to make the Home page the first page in the stack.

LoginPage

import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:my_first_app/home.dart';

class LoginPage extends StatefulWidget {
  LoginPage({Key key}) : super(key: key);

  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final GlobalKey<FormState> _loginFormKey = GlobalKey<FormState>();
  TextEditingController emailInputController;
  TextEditingController pwdInputController;

  @override
  initState() {
    emailInputController = new TextEditingController();
    pwdInputController = new TextEditingController();
    super.initState();
  }

  String emailValidator(String value) {
    Pattern pattern =
        r'^(([^<>()[]\.,;:s@"]+(.[^<>()[]\.,;:s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))$';
    RegExp regex = new RegExp(pattern);
    if (!regex.hasMatch(value)) {
      return 'Email format is invalid';
    } else {
      return null;
    }
  }

  String pwdValidator(String value) {
    if (value.length < 8) {
      return 'Password must be longer than 8 characters';
    } else {
      return null;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Login"),
        ),
        body: Container(
            padding: const EdgeInsets.all(20.0),
            child: SingleChildScrollView(
                child: Form(
              key: _loginFormKey,
              child: Column(
                children: <Widget>[
                  TextFormField(
                    decoration: InputDecoration(
                        labelText: 'Email*', hintText: "[email protected]"),
                    controller: emailInputController,
                    keyboardType: TextInputType.emailAddress,
                    validator: emailValidator,
                  ),
                  TextFormField(
                    decoration: InputDecoration(
                        labelText: 'Password*', hintText: "********"),
                    controller: pwdInputController,
                    obscureText: true,
                    validator: pwdValidator,
                  ),
                  RaisedButton(
                    child: Text("Login"),
                    color: Theme.of(context).primaryColor,
                    textColor: Colors.white,
                    onPressed: () {
                      if (_loginFormKey.currentState.validate()) {
                        FirebaseAuth.instance
                            .signInWithEmailAndPassword(
                                email: emailInputController.text,
                                password: pwdInputController.text)
                            .then((currentUser) => Firestore.instance
                                .collection("users")
                                .document(currentUser.uid)
                                .get()
                                .then((DocumentSnapshot result) =>
                                    Navigator.pushReplacement(
                                        context,
                                        MaterialPageRoute(
                                            builder: (context) => HomePage(
                                                  title: result["fname"] +
                                                      "'s Tasks",
                                                  uid: currentUser.uid,
                                                ))))
                                .catchError((err) => print(err)))
                            .catchError((err) => print(err));
                      }
                    },
                  ),
                  Text("Don't have an account yet?"),
                  FlatButton(
                    child: Text("Register here!"),
                    onPressed: () {
                      Navigator.pushNamed(context, "/register");
                    },
                  )
                ],
              ),
            ))));
  }
}
  1. The UI for this page is very similar to the Register page. The minor difference is that only the email and password of the user is captured. Both TextFormFields have their own controllers and validation methods, like the Register page. However, the RaisedButton Widget is used to log in the user. TheFlatButton Widget included redirects the user to the Register page if they don’t have an account. The form has its own GlobalKey, to keep track of the forms state.
  2. When the Login button is pressed, the form is first validated. If the validation is a success, the signInWithEmailAndPassword method of the FirebaseAuth instance is used to log in the user.
  3. If the user successfully logs in, the user’s document from the collection users in Cloud Firestore is fetched.
  4. If both Firebase actions are a success, the user is directed to the Home page, with the user’s first name included in the title.
  5. The Navigator method pushReplacement is used to make the Home page the first page in the stack.

Step 4 — Updating the Home page

We’ve changed the location of the task collection. It’s now a collection within a user document from one single collection. We’ll now need to update the Home page code to handle that change. We also need to update the state of the Home page to accept the uid passed to it.

The Home page is updated to the code below:

The two areas where the task collection is accessed needs to be updated to are:

The stream object snapshots:

And the reference to the collection to which we push the new tasks created:

The value widget.uid is the uid passed to the Home page when the user registers or logs in.

Demo

Now to have a look at the final product (while skipping the tedious data entry 🤭):

We can find a new user signed up through the app in the Authentication page of the dashboard:

We’ll also find a new document in the Cloud Firestore collection users:

As you can see above, the new user’s uid is used as the document id of that specific user’s document in the ‘users’ collection.

So, to recap

We were able to:

  1. Allow user authentication from the Firebase Console
  2. Integrate Firebase Authentication in the app
  3. Change the Navigation flow of the App to include the authentication process
  4. Create the new pages Splash, Login, and Register
  5. Register a new user and create a custom document in the Cloud Firestore collection user
  6. Fetch tasks from and add new tasks to that specific user object

Fritz

Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *

wix banner square