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
andgoogle-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 aFirebaseUser
, 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 typeFirebaseUser
. This takesonAuthStateChanged
as an input stream, and calls thebuilder
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 course.
Happy coding!