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:
StreamBuilder
to present different screens depending on the authentication status of the user.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:
GoogleServices-info.plist
and google-services.json
to the iOS and Android projects, otherwise the app will crash at startup.I cover all these steps in detail in my Flutter & Firebase course.
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.
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…
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:
FirebaseAuth.instance.onAuthStateChanged
stream. This receives a new value each time the user signs in or out.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:
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…
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()
.
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 ;)
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.
#flutter #firebase #firebaseauth