Flutter app development tutorials by Andrea Bizzotto

Super Simple Authentication Flow with Flutter & Firebase

User authentication is a very common requirement for a lot of apps.

In this article we implement a simple authentication flow in Flutter, in less than 100 lines of code.

As part of this, we will see how to:

  • use FirebaseAuth to sign in anonymously.
  • use StreamBuilder to present different screens depending on the authentication status of the user.

This is the basis of my Reference Authentication Flow with Flutter & Firebase on GitHub.

So, let's start from the basics.

Initial setup

We will use Firebase Authentication for this example.

After creating a new Flutter project, we can add firebase_auth to the dependencies section of our pubspec.yaml file:

// pubspec.yaml dependencies: flutter: sdk: flutter firebase_auth: 0.11.1+3

Then, we need to configure our Flutter app to use Firebase. This guide explains what to do step-by-step:

The two most important steps are:

  • Add GoogleServices-info.plist and google-services.json to the iOS and Android projects, otherwise the app will crash at startup.
  • Enable anonymous sign-in in the Firebase console, as we will use it in this example.

I cover all these steps in detail in my Flutter & Firebase course.

Let's code

Our application will have two pages, called SignInPage and HomePage, which are both stateless widgets:

Then we will have another widget called LandingPage. We will use this to decide which page to show depending on the authentication status of the user.

Here is the entire widget tree of this app:

Let's implement this in code.

SignInPage

First, the SignInPage:

import 'package:flutter/material.dart'; import 'package:firebase_auth/firebase_auth.dart'; class SignInPage extends StatelessWidget { Future<void> _signInAnonymously() async { try { await FirebaseAuth.instance.signInAnonymously(); } catch (e) { print(e); // TODO: show dialog with error } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Sign in')), body: Center( child: RaisedButton( child: Text('Sign in anonymously'), onPressed: _signInAnonymously, ), ), ); } }

All this does is to show a centered RaisedButton, which calls _signInAnonymously() when pressed.

This method calls FirebaseAuth.instance.signInAnonymously() and awaits for the result.

NOTES

  • try/catch is used to catch any exceptions. We can use this to alert the user if sign-in fails.
  • await FirebaseAuth.instance.signInAnonymously() returns a FirebaseUser, but our code doesn't use the return value. That's because we will handle the authentication status of the user somewhere else.

Speaking of which...

LandingPage

We use this widget class to decide which page to show:

import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; class LandingPage extends StatelessWidget { @override Widget build(BuildContext context) { return StreamBuilder<FirebaseUser>( stream: FirebaseAuth.instance.onAuthStateChanged, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.active) { FirebaseUser user = snapshot.data; if (user == null) { return SignInPage(); } return HomePage(); } else { return Scaffold( body: Center( child: CircularProgressIndicator(), ), ); } }, ); } }

This page uses two main ingredients:

  • The FirebaseAuth.instance.onAuthStateChanged stream. This receives a new value each time the user signs in or out.
  • A StreamBuilder of type FirebaseUser. This takes onAuthStateChanged as an input stream, and calls the builder when the stream is updated.

So when a call to FirebaseAuth.instance.signInAnonymously() succeeds, a new FirebaseUser is added to onAuthStateChanged.

As a result, the builder is called and we can extract the FirebaseUser from snapshot.data. And we use this to decide which page to show:

FirebaseUser user = snapshot.data; if (user == null) { return SignInPage(); } return HomePage();

Also note how we're checking the connectionState of the snapshot:

if (snapshot.connectionState == ConnectionState.active) { // do something }

This can be any of four possible values: none, waiting, active, done.

When the application starts, the builder is first called with ConnectionState.waiting. We can use this to show a centered CircularProgressIndicator().

Once the authentication status is determined, the connectionState becomes active, and our builder is called again.

In summary, we have three possible authentication states:

  • unknown
  • user signed-in
  • user not signed-in

And this code is all we need to handle them:

if (snapshot.connectionState == ConnectionState.active) { FirebaseUser user = snapshot.data; if (user == null) { return SignInPage(); } return HomePage(); } else { return Scaffold( body: Center( child: CircularProgressIndicator(), ), ); }

Moving on...

HomePage

This class is similar to the SignInPage:

import 'dart:async'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; class HomePage extends StatelessWidget { Future<void> _signOut() async { try { await FirebaseAuth.instance.signOut(); } catch (e) { print(e); // TODO: show dialog with error } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Home Page'), actions: <Widget>[ FlatButton( child: Text( 'Logout', style: TextStyle( fontSize: 18.0, color: Colors.white, ), ), onPressed: _signOut, ), ], ), ); } }

This code calls FirebaseAuth.instance.signOut() when the logout button is pressed.

On success, a null value is added to onAuthStateChanged. As a result, the builder in our LandingPage is called again, and this time we return a SignInPage().

Finishing up

Almost there. Now we just need to update our main.dart file, to pass the LandingPage() to the home argument of MaterialApp:

import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.indigo, ), home: LandingPage(), ); } }

All in all, this entire flow takes less than 100 lines of code.

Here is the code for the entire example:

import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.indigo, ), home: LandingPage(), ); } } class LandingPage extends StatelessWidget { @override Widget build(BuildContext context) { return StreamBuilder<FirebaseUser>( stream: FirebaseAuth.instance.onAuthStateChanged, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.active) { FirebaseUser user = snapshot.data; if (user == null) { return SignInPage(); } return HomePage(); } else { return Scaffold( body: Center( child: CircularProgressIndicator(), ), ); } }, ); } } class SignInPage extends StatelessWidget { Future<void> _signInAnonymously() async { try { await FirebaseAuth.instance.signInAnonymously(); } catch (e) { print(e); // TODO: show dialog with error } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Sign in')), body: Center( child: RaisedButton( child: Text('Sign in anonymously'), onPressed: _signInAnonymously, ), ), ); } } class HomePage extends StatelessWidget { Future<void> _signOut() async { try { await FirebaseAuth.instance.signOut(); } catch (e) { print(e); // TODO: show dialog with error } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Home Page'), actions: <Widget>[ FlatButton( child: Text( 'Logout', style: TextStyle( fontSize: 18.0, color: Colors.white, ), ), onPressed: _signOut, ), ], ), ); } }

This uses just a single main.dart file. I advise to put widget classes in separate files in your own projects ;)

Conclusion

We have seen how to build a simple authentication flow with Firebase.

This example doesn't use any fancy app architecture.

And sometimes, keeping things simple is a good idea. As Albert Einstein once said:

Everything Should Be Made as Simple as Possible, But Not Simpler

However, Einstein wasn't a software developer. 😄

And the code I presented has two major drawbacks:

1) Global Access

The LandingPage, SignInPage, HomePage all access FirebaseAuth via the instance singleton variable.

This is not recommended because the resulting code is not testable.

2) Direct use of FirebaseAuth

Using FirebaseAuth directly in our widgets is not a good idea.

This can cause problems if our application grows, and we decide to use a different authentication provider in the future.


In the next article we will see how to address these concerns. We will do this by:

  • moving from global access to scoped access with Provider
  • writing an authentication service class as a wrapper for FirebaseAuth

By the way, I cover all these topics (and much more) in my Flutter & Firebase Udemy course.

Happy coding!

Want more?

Support my work and fast-track your Flutter learning with my in-depth courses.