Flutter : How to do user login with Firebase

Release

25/12/18 — Updated latest code snippet after refactoring and clean up.

24/01/19 — Duplicated link to github at the top of article.

Source Code

In case you want to skip the whole mumbo jumbo, you can grab the source code here 👇

https://github.com/tattwei46/flutter_login_demo

Update

Here is a sequel to this post which is How to do CRUD with Firebase RTDB. Check it out!

What is Flutter?

Flutter is an open source mobile SDK developed by Google to build high quality applications for Android and iOS. It allows developers to not only build application with beautiful design, smooth animation and fast performance, but also able to integrate new features quickly. Flutter offers high velocity development with its stateful hot reload and hot restart. With only one code base to manage, you get to save a lot of cost comparing to managing both Android and iOS projects as Flutter compiles it to native ARM code. Flutter uses Dart programming language which is also developed by Google.

Why Dart?

  • A terse, strongly-typed, object-oriented language.
  • Supports just-in-time and ahead-of-time compilation.
  • JIT allows flutter to recompile code directly on the device while the app is still running.
  • Enable fast development and enables sub-second stable hot reloading.
  • AOT allows code to be compiled directly into native ARM code leading to fast startup and predictable performance.

What is Firebase

Firebase is a mobile and web development platform that provides developer with wide range of products. Today we will be looking how to build our first flutter application with Firebase authentication and realtime database. This application allows user to signup or login and perform todo items CRUD with Firebase. On this post, we are going to solely focus on the user signup and login part.

How to setup environment

  • Follow the instructions in the this link
  • Get the Flutter SDK
  • Run Flutter doctor to install any dependencies
flutter doctor
  • Use the command below to open iOS simulator
open -a Simulator
  • To open android emulator, launch Android Studio > tools > AVD Manager and select create Virtual Device.

Building Flutter App

You can get the complete source code in the GitHub link at the bottom of the post. The following shows how do we derive from Flutter sample project to complete source code in GitHub.

👉Step 1: Create a new flutter project call flutter login demo. Launch simulator and run project using flutter. You can use either Android Studio or VSCode as your preferred IDE. Steps to configure your editor in here.

flutter run

If you have both Android emulator and iOS Simulator running, run the following command to execute on both.

flutter run -d all

You should see similar screens on both Android Emulator and iOS Simulator.

Left: Android, Right: iOS

If you’re interested to know how to get screenshots at your simulators;

For Android: Simply click the camera icon on the left side of the tool pane. The image will be saved to desktop

For iOS: Hold and press command + shift + 4. Press the space bar to change Mouse pointer to camera icon. Point to iOS simulator, click to take screenshot. The image will be saved to desktop.

👉Step 2: At main.dart, erase all contents and add the following boilerplate to your file. We are going to create a new file called login_page.dart which has LoginPage class. On your terminal, hit the R key to perform hot reload and you should see “Hello World” on the screen.

Main.dart

import 'package:flutter/material.dart';
import 'login_signup_page.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
return new MaterialApp(
      title: 'Flutter Login Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new LoginSignUpPage()
    );
  }
}

login_signup_page.dart

import 'package:flutter/material.dart';

class LoginSignUpPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
return new Scaffold(
      appBar: new AppBar(
        title: new Text("Flutter login demo"),
      ),
      body: new Container(
        child: new Text("Hello World"),
      ),
    );
  }
}

👉Step 3: Changing from stateless to stateful.

login_signup_page.dart

import 'package:flutter/material.dart';

class LoginSignUpPage extends StatefulWidget {

  @override
  State<StatefulWidget> createState() => new _LoginSignUpPageState();

}

class _LoginSignUpPageState extends State<LoginSignUpPage>{

  @override
  Widget build(BuildContext context) {
return new Scaffold(
      appBar: new AppBar(
        title: new Text("Flutter login demo"),
      ),
      body: new Container(
        child: new Text("Hello World"),
      ),
    );
  }
}

👉Step 4: Inside the Scaffold body, let’s replace the Hello Word text to a Form and inside it we will put a ListView. A ListView takes an array of widgets. We will refactor out each UI component into separate widget.

Whenever we are using text input, it is better to wrap it around a ListView to prevent rendering error when the soft keyboard shows up due to overflow pixels.

login_signup_page.dart

@override
Widget build(BuildContext context) {
  _isIos = Theme.of(context).platform == TargetPlatform.iOS;
return new Scaffold(
      appBar: new AppBar(
        title: new Text('Flutter login demo'),
      ),
      body: Stack(
        children: <Widget>[
          _showBody(),
          _showCircularProgress(),
        ],
      ));
}

👉Step 5: Building each UI components

Notice in the Scaffold body, we have a Stack widget as the body. Basically what I want to do is to show user a circular loading indicator, when any login or sign up activity is running. To do this, we need to overlay a CircularProgressIndicator (thankfully, Flutter already has this widget, so to use it, just simply call it) with our main widget layout (the login/signup form). That is the function of the Stack widget, which allows one widget to overlay on top of another widget. To control whether to show the CircularProgressIndicator or not, we check on bool _isLoading whether now the screen is loading or not.

Widget _showCircularProgress(){
if (_isLoading) {
return Center(child: CircularProgressIndicator());
  } return Container(height: 0.0, width: 0.0,);

}

For the logo, we will use a hero widget and also to import the image by adding the following line in your pubspec.yaml. Then run get packages to import your image.

assets:
- assets/flutter-icon.png

login_signup_page.dart

Widget _showLogo() {
return new Hero(
    tag: 'hero',
    child: Padding(
      padding: EdgeInsets.fromLTRB(0.0, 70.0, 0.0, 0.0),
      child: CircleAvatar(
        backgroundColor: Colors.transparent,
        radius: 48.0,
        child: Image.asset('assets/flutter-icon.png'),
      ),
    ),
  );
}

[Update] Previously we use flexible spacing widget using SizedBox that takes in height input to have a vertical spacing between the 2 widgets. Now we just place one widget inside a padding widget and use padding: EdgeInsets.fromLTRB() which means from left, top, right and bottom and enter the value of padding at correct position accordingly.

Next comes our email and password text form field. Notice for each field we have validator and onSaved. These 2 callbacks will be triggered when form.validate() and form.save() is called. So for example if form.save() is called, the value in the text form field is copied into another local variable.

We will also introduce a validator to our fields to check if field input is empty and then show warning to user in red. We also need to create variables _email and _password to store the values. For password, we set obsecureText: true to hide user password.

Widget _showEmailInput() {
return Padding(
    padding: const EdgeInsets.fromLTRB(0.0, 100.0, 0.0, 0.0),
    child: new TextFormField(
      maxLines: 1,
      keyboardType: TextInputType.emailAddress,
      autofocus: false,
      decoration: new InputDecoration(
          hintText: 'Email',
          icon: new Icon(
            Icons.mail,
            color: Colors.grey,
          )),
      validator: (value) => value.isEmpty ? 'Email can't be empty' : null,
      onSaved: (value) => _email = value,
    ),
  );
}

Widget _showPasswordInput() {
return Padding(
    padding: const EdgeInsets.fromLTRB(0.0, 15.0, 0.0, 0.0),
    child: new TextFormField(
      maxLines: 1,
      obscureText: true,
      autofocus: false,
      decoration: new InputDecoration(
          hintText: 'Password',
          icon: new Icon(
            Icons.lock,
            color: Colors.grey,
          )),
      validator: (value) => value.isEmpty ? 'Password can't be empty' : null,
      onSaved: (value) => _password = value,
    ),
  );
}

Next we need to add the primary button but it should be able to display the correct text depending on whether the user wants to sign up for new account or login with existing account. For this, we need to create an enum to keep track whether the form is for login or signup.

enum FormMode { LOGIN, SIGNUP }

We will assign a method for the button callback function. For this, we will create method called _validateAndSubmit that will pass in both email and password for Firebase authentication. More on that later in this post.

Widget _showPrimaryButton() {
return new Padding(
      padding: EdgeInsets.fromLTRB(0.0, 45.0, 0.0, 0.0),
      child: new MaterialButton(
        elevation: 5.0,
        minWidth: 200.0,
        height: 42.0,
        color: Colors.blue,
        child: _formMode == FormMode.LOGIN
            ? new Text('Login',
                style: new TextStyle(fontSize: 20.0, color: Colors.white))
            : new Text('Create account',
                style: new TextStyle(fontSize: 20.0, color: Colors.white)),
        onPressed: _validateAndSubmit,
      ));
}

Now we need to add a secondary button for user to be able to toggle between signup and login form. On the onPressed method, we would like to toggle the state of the form between LOGIN and SIGNUP. Notice for secondary button, we are using FlatButton instead of RaisedButton like the previous submit button. The reason is if you have 2 buttons and would like to make 1 more distinctive than the other, RaisedButton is the right choice as it instantly catches the users’ attention compare to FlatButton.

login_page.dart

Widget _showSecondaryButton() {
return new FlatButton(
    child: _formMode == FormMode.LOGIN
        ? new Text('Create an account',
            style: new TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300))
        : new Text('Have an account? Sign in',
            style:
new TextStyle(fontSize: 18.0, fontWeight: FontWeight.w300)),
    onPressed: _formMode == FormMode.LOGIN
        ? _changeFormToSignUp
        : _changeFormToLogin,
  );
}

On the method to toggle form mode, it is crucial to wrap it around setState as we need to tell Flutter to re-render the screen with updated value of the FormMode.

void _changeFormToSignUp() {
  _formKey.currentState.reset();
  _errorMessage = "";
  setState(() {
    _formMode = FormMode.SIGNUP;
  });
}

void _changeFormToLogin() {
  _formKey.currentState.reset();
  _errorMessage = "";
  setState(() {
    _formMode = FormMode.LOGIN;
  });
}

Next, we are going to have a _showErrorMessage() that pass the error message to user from Firebase side when they are trying to do login or signup. This error message could be something like “There is already an existing user account”. So we are going to have a String _errorMessage to store the error message from Firebase.

Widget _showErrorMessage() {
if (_errorMessage.length > 0 && _errorMessage != null) {
return new Text(
      _errorMessage,
      style: TextStyle(
          fontSize: 13.0,
          color: Colors.red,
          height: 1.0,
          fontWeight: FontWeight.w300),
    );
  } else {
return new Container(
      height: 0.0,
    );
  }
}

Finally, let’s arrange those individual UI components and put it back to our ListView.

Widget _showBody(){
return new Container(
      padding: EdgeInsets.all(16.0),
      child: new Form(
        key: _formKey,
        child: new ListView(
          shrinkWrap: true,
          children: <Widget>[
            _showLogo(),
            _showEmailInput(),
            _showPasswordInput(),
            _showPrimaryButton(),
            _showSecondaryButton(),
            _showErrorMessage(),
          ],
        ),
      ));
}
 
TextFormField validator in action

👉Step 6: Register new project with Firebase

Go to https://console.firebase.google.com and register new project.

For android, click the android icon. Enter your package name which can be found in android/app/src/main/AndroidManifest.xml

Download the config file which is google-services.json (Android).

Drag the google-services.json into app folder in project view

We need to add the Google Services Gradle plugin to read google-services.json. In the /android/app/build.gradle add the following to the last line of the file.

apply plugin: 'com.google.gms.google-services'

In android/build.gradle, inside the buildscript tag, add new dependency.

buildscript {
   repositories {
      //...
}
dependencies {
   //...
   classpath 'com.google.gms:google-services:3.2.1'
}

For iOS, open ios/Runner.xcworkspace to launch Xcode. The package name can be found in bundle identifier at Runner view.

Download the config file which is GoogleService-info.plist (iOS).

Drag the GoogleService-info.plist into the Runner subfolder inside Runner as shown below.

👉Step 7: Add dependencies in pubspec.yaml
Next we need to add firebase_auth dependency in pubspec.yaml. To get the latest version number, go to https://pub.dartlang.org/ and search for firebase auth.

firebase_auth: ^0.6.6

👉Step 8: Import Firebase Auth

import 'package:firebase_auth/firebase_auth.dart';

👉Step 9: Enable sign up using email and password at Firebase

👉Step 10: Sign in to Firebase

Firebase signInWithEmailAndPassword is a method that returns a future value. Hence the method needs to have await and the external wrapper function needs to have async. So we enclose the login and signup methods with try catch block. If there was an error, our catch block should be able to capture the error message and display to the user.

There is a difference in how the actual message is store in the error thrown by Firebase. In IOS, the message is in e.details whereas for Android, is in e.message. You can easily check the platform by using _isIos = Theme.of(context).platform == TargetPlatform.iOS and it should be inside any of the build widget method because it needs a context.

_validateAndSubmit() async {
  setState(() {
    _errorMessage = "";
    _isLoading = true;
  });
if (_validateAndSave()) {
    String userId = "";
try {
if (_formMode == FormMode.LOGIN) {
        userId = await widget.auth.signIn(_email, _password);
        print('Signed in: $userId');
      } else {
        userId = await widget.auth.signUp(_email, _password);
        print('Signed up user: $userId');
      }
if (userId.length > 0 && userId != null) {
        widget.onSignedIn();
      }
    } catch (e) {
      print('Error: $e');
      setState(() {
        _isLoading = false;
if (_isIos) {
          _errorMessage = e.details;
        } else
_errorMessage = e.message;
      });
    }
  }
}

👉Step 11: Clear form field when toggle

We need to add the following line in both _changeFormToSignUp and _changeFormToLogin to reset form field everytime user switches between login and sign up form.

formKey.currentState.reset();

👉Step 12: Try to sign up a user

Let’s try to sign up a user by entering an email and password.

If you encounter something like below, this is because there is an extra spacing at the end of your email

I/flutter (14294): Error PlatformException(exception, The email address is badly formatted., null)

If you encounter something like below, change your password to be at least 6 characters long.

I/flutter (14294): Error PlatformException(exception, The given password is invalid. [ Password should be at least 6 characters ], null)

Finally once success, you should be able to see in your terminal the following line. The random string is the user ID.

I/flutter (14294): Signed up JSwpKsCFxPZHEqeuIO4axCsmWuP2

Similarly if we try to sign in the same user we signed up, we should get something like this:

I/flutter (14294): Signed in JSwpKsCFxPZHEqeuIO4axCsmWuP2

👉Step 13: Implement Auth class

Create new file call authentication.dart. We are also going to implement abstract class BaseAuth. Purpose of this abstract class is it acts as a middle layer between our UI components and the actual implementation class which depends on the framework we choose. In any case, we decided to swap Firebase to something like PostgreSQL, then it would not have impact on the UI components.

import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';

abstract class BaseAuth {
  Future<String> signIn(String email, String password);
  Future<String> signUp(String email, String password);
  Future<String> getCurrentUser();
  Future<void> signOut();
}

class Auth implements BaseAuth {
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;

  Future<String> signIn(String email, String password) async {
    FirebaseUser user = await _firebaseAuth.signInWithEmailAndPassword(email: email, password: password);
return user.uid;
  }

  Future<String> signUp(String email, String password) async {
    FirebaseUser user = await _firebaseAuth.createUserWithEmailAndPassword(email: email, password: password);
return user.uid;
  }

  Future<String> getCurrentUser() async {
    FirebaseUser user = await _firebaseAuth.currentUser();
return user.uid;
  }

  Future<void> signOut() async {
return _firebaseAuth.signOut();
  }
}

In login_page.dart

class LoginSignUpPage extends StatefulWidget {
LoginSignUpPage({this.auth});
final BaseAuth auth;
@override
State<StatefulWidget> createState() => new _LoginPageState();
}

In main.dart

home: new LoginSignUpPage(auth: new Auth())

Back in login_page.dart, lets swap our signInWithEmailAndPassword

String userId = await widget.auth.signIn(_email, _password);
String userId= await widget.auth.signUp(_email, _password);

👉Step 14: Root and home with VoidCallback

Let’s create a new file call home_page.dart. This will display empty to do list after user successfully signed in or registered. As usual, we implement Scaffold with AppBar but this time we are going to have a FlatButton inside AppBar for logout function. This logout calls the Firebase signout method inside BaseAuth class.

We also need to create a file call root_page.dart. This will be replacing home: LoginSignUpPage(auth: new Auth()) in our main.dart.

home: new RootPage(auth: new Auth())

When the app launches, it should navigate to this page. This page acts as a manager to check for valid Firebase user id and directs them to appropriate page according. For example if the user id is present, meaning user is already signed in and user should be shown the home_page instead of login_signup_page. This will be done inside initState which is the function that will be executed first in the file.

In the root_page, there will be 2 methods which is the _onLoggedIn and _onSignedOut. In the _onLoggedIn , we try to get the user id and setstate authStatus to user is already logged in. In the _onSignedOut , we clear the stored user id and setstate authStatus to user is not logged in.

In the root_page, we pass in 2 parameters into login_page, one is the Auth class that we implement easier (we instantiate it at main.dart) and _onLoggedIn method). In the login_signup_page, we create 2 variables which is auth of type BaseAuth and onSignedIn of type VoidCallback. We can easily retrieve the 2 parameters passed into login_signup_page into our local variables using the following line.

LoginSignUpPage({this.auth, this.onSignedIn});

final BaseAuth auth;
final VoidCallback onSignedIn;

VoidCallback allows login_signup_page to call the method inside root_page which is the _onSignedIn when the user signs in. When _onSignedIn is called, it will set the authStatus to LOGGED_IN and setState to redraw the app. When the app is redrawn, initState checks the authStatus and since it is LOGGED_IN, it will show home_page, passing in auth and voidcallback of _signOut.

root_page.dart

@override
Widget build(BuildContext context) {
switch (authStatus) {
case AuthStatus.NOT_DETERMINED:
return _buildWaitingScreen();
break;
case AuthStatus.NOT_LOGGED_IN:
return new LoginSignUpPage(
        auth: widget.auth,
        onSignedIn: _onLoggedIn,
      );
break;
case AuthStatus.LOGGED_IN:
if (_userId.length > 0 && _userId != null) {
return new HomePage(
          userId: _userId,
          auth: widget.auth,
          onSignedOut: _onSignedOut,
        );
      } else return _buildWaitingScreen();
break;
default:
return _buildWaitingScreen();
  }
}

Notice the debug ribbon at the top right corner of the app, you can easily remove it by adding the following line inside MaterialApp widget in main.dart

 
Demo login screen
debugShowCheckedModeBanner: false,
 
Debug banner removed

You can get the complete source code in the github link below

If you find this article is useful, give some 👏

Reference:


The Flutter Pub is a medium publication to bring you the latest and amazing resources such as articles, videos, codes, podcasts etc. about this great technology to teach you how to build beautiful apps with it. You can find us on Facebook, Twitter, and Medium or learn more about us here. We’d love to connect! And if you are a writer interested in writing for us, then you can do so through these guidelines.

About the author

Founder of tattweicheah.com. Loves music, sport and most importantly software development.

Leave a Reply

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