In this piece, we will create a simple application with the following components:
There are plenty of examples online about setting up Firebase for Flutter so I will jump right into the code instead of walking thru the basics.
See Google CodeLabs Flutter for Firebase for step-by-step instructions for setting up you project on iOS or Android
Since we are just building the application and there is no functionality to create users right now, log in to you Firebase Console and add a user to your project. Be sure to enable email authentication when updating the project in your Firebase Console.
First, let’s create the project:
flutter create simple_firebase_auth
Now let’s do some project cleanup, open up the project and delete the existing HomePage
and HomePageState
widget from the file main.dart
.
Change the home
property of the MaterialApp
widget to point to the LoginPage
widget we are about to create in the next section.
The file should look similar to this when completed:
import 'package:flutter/material.dart';
import 'package:simple_firebase_auth/login_page.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: LoginPage(),
);
}
}
Markdium-javascript.js
Let’s walk through the creation of the LoginPage
for the application. We need to capture an email
and a password
to pass to the AuthService
to call the login function.
We are going to create a simple page with the required TextFormField
widgets and one RaisedButton
that will log a user in when clicked.
lib
directory named login_page.dart
login_page.dart
import 'package:flutter/material.dart';
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Login Page Flutter Firebase"),
),
body: Center(
child: Text('Login Page Flutter Firebase Content'),
),
);
}
}
Markdium-javascript.js
You should be able to run the code to see what the screen looks like now. Be sure to change the default route or home
property in main.dart
widget to LoginPage
while we work through the UI so you can see the changes update live.
Let’s make the body of the page a centered Column
with the children of the column being primarily the TextFormField
and the RaisedButton
.
The centered container to hold the form fields and buttons:
body: Container(
padding: EdgeInsets.all(20.0),
child: Column()
)
Markdium-javascript.js
Next, add the actual form field widgets and the buttons as children of the Column
widget. We will do some basic styling of the form fields so that this looks presentable. See the Flutter documentation for more information on TextFormFields.
body: Container(
padding: EdgeInsets.all(20.0),
child: Column(
children: <Widget>[
Text(
'Login Information',
style: TextStyle(fontSize: 20),
),
TextFormField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(labelText: "Email Address")),
TextFormField(
obscureText: true,
decoration: InputDecoration(labelText: "Password")),
RaisedButton(child: Text("LOGIN"), onPressed: () {}),
],
),
),
Markdium-javascript.js
Let’s add some spacing between the fields in the column so it is more presentable. We are going to use the SizedBox
widget and set the height
property to get some spacing in the application. Replace the children
property of the Column
widget to get the desired spacing.
children: <Widget>[
SizedBox(height: 20.0), // <= NEW
Text(
'Login Information',
style: TextStyle(fontSize: 20),
),
SizedBox(height: 20.0), // <= NEW
TextFormField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(labelText: "Email Address")),
TextFormField(
obscureText: true,
decoration: InputDecoration(labelText: "Password")),
SizedBox(height: 20.0), // <= NEW
RaisedButton(child: Text("LOGIN"), onPressed: () {}),
],
Markdium-javascript.js
We are going to be using a Form
widget and a GlobalKey
, additional information on these concepts can be found in the flutter cookbook section Building a form with validation.
Add the formKey in the LoginPage
widget:
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
Markdium-javascript.js
Then add two new fields to hold the email address and password values we will need to send to Firebase for authentication:
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
String _password;
String _email;
Markdium-javascript.js
Next, add a property onSaved
to the TextFormFields
we have for email and password. When the save
method is called on the form, all of the widgets onSaved methods will be called to update the local variables.
TextFormField(
onSaved: (value) => _email = value, // <= NEW
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(labelText: "Email Address")),
TextFormField(
onSaved: (value) => _password = value, // <= NEW
obscureText: true,
decoration: InputDecoration(labelText: "Password")),
Markdium-javascript.js
Wrap the Column
widget with a new Form
widget, the code should look similar to this:
body: Container(
padding: EdgeInsets.all(20.0),
child: Form( // <= NEW
key: _formKey, // <= NEW
child: Column(
children: <Widget>[
....
],
),
),
),
Markdium-javascript.js
Now that the fields are set and the TextFormField
are updated, we can use the _formKey
to not only validate the fields provided but also retrieve the values locally by calling the save
method.
Replace the code in the RaisedButton
onPressed
method to the following, and you will see that we are getting the values for email and password set in out widget. We can now pass these values to the AuthService
that wraps the Firebase sign-in functionality.
// save the fields..
final form = _formKey.currentState;
form.save();
// Validate will return true if is valid, or false if invalid.
if (form.validate()) {
print("$_email $_password");
}
Markdium-javascript.js
For now, we will keep the home page simple, since our goal is to demonstrate how the flow works. Ignore the commented out LogoutButton
widget, we will discuss that in a later section of the tutorial.
lib
directory named home_page.dart
home_page.dart
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Home Flutter Firebase"),
//actions: <Widget>[LogoutButton()],
),
body: Center(
child: Text('Home Page Flutter Firebase Content'),
),
);
}
}
Markdium-javascript.js
main.dart
and add the following import statementimport 'home_page.dart';
Markdium-javascript.js
home
property from this:home: HomePage(title: 'Flutter Demo Home Page'),
Markdium-javascript.js
To this:
home: HomePage(),
Markdium-javascript.js
Here we will build out the authentication service separate from Firebase, validate that everything works and then integrate Firebase.
In this service, we are using a mixin called ChangeNotifier
and a method notifyListeners
which will allow for the widgets that are using this Service to be updated when the method is called. We are calling notifyListeners
when we update the currentUser
property because that means that the user has either logged in or logged out and we want the application to update based on the user’s state.
More information on
Provider
and State Management can be found here in the Flutter Documentation
What we need as a baseline is the following:
import 'dart:async';
import 'package:flutter/material.dart';
class AuthService with ChangeNotifier {
var currentUser;
AuthService() {
print("new AuthService");
}
Future getUser() {
return Future.value(currentUser);
}
// wrappinhg the firebase calls
Future logout() {
this.currentUser = null;
notifyListeners();
return Future.value(currentUser);
}
// wrapping the firebase calls
Future createUser(
{String firstName,
String lastName,
String email,
String password}) async {}
// logs in the user if password matches
Future loginUser({String email, String password}) {
if (password == 'password123') {
this.currentUser = {'email': email};
notifyListeners();
return Future.value(currentUser);
} else {
this.currentUser = null;
return Future.value(null);
}
}
}
Markdium-javascript.js
We will keep a local property in the service call currentUser
which is the object storing the user when the user calls the login
method and if the password matches we will set currentUser
and the user will be logged in. This will now provide a user when the call is made to getUser
method. For logging the user out, we will set the currentUser
property to null indicating that we are no longer logged into the system.
The first challenge when working with the application is to determine which page to open when the application starts up. What we want to do here is determine if we have a user or not. We will be using an AuthService
we created above combined with the FutureBuilder
widget from Flutter to render the correct first page of either a HomePage
or a LoginPage
.
In main.dart
we will need to update the default main
method to look like this; we are wrapping the whole application with the ChangeNotifierProvider
to get the ability to scan up the widget tree and find an object of type AuthService
.
void main() => runApp(
ChangeNotifierProvider<AuthService>(
child: MyApp(),
builder: (BuildContext context) {
return AuthService();
},
),
);
Markdium-javascript.js
Go into the main.dart
and make the following changes that will allow the MyApp
widget to set the route. This widget will determine if the application should navigate to the HomePage
widget or LoginPage
widget when the app is launched.
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: FutureBuilder(
// get the Provider, and call the getUser method
future: Provider.of<AuthService>(context).getUser(),
// wait for the future to resolve and render the appropriate
// widget for HomePage or LoginPage
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return snapshot.hasData ? HomePage() : LoginPage();
} else {
return Container(color: Colors.white);
}
},
),
);
}
}
Markdium-javascript.js
Now that the AuthService
can be accessed using the Provider, we can call the login function when the used clicks the button. Open the file login_page.dart
and find the onPressed
method for the login button and make the following change
// Validate will return true if is valid, or false if invalid.
if (form.validate()) {
var result = await Provider.of(context)
.loginUser(email: _email, password: _password);
if (result == null) {
// see project in github for this code
//return _buildShowErrorDialog(context,
// "Error Logging In With Those Credentials");
}
}
Markdium-Dart.dart
We are using the Provider.of
method to look up the widget tree and get our AuthService
and then we have access to all of the methods, specifically the loginUser
method.
At this point, you should have a functioning application with the basic login flow where the user will be logged in successfully if you provide the password as password123.
I added in a little extra functionality of displaying an error message in a dialog box if some sort of error occurs.
Now we will integrate firebase into the application.
There are plenty of examples online about setting up Firebase for Flutter, so I will jump right into the code instead of walking through the basics. See Google CodeLabs Flutter for Firebase for step-by-step instructions for setting up your project on iOS or Android.
Since we are just building the application, and there is no functionality to create users in the application right now, please log in to your Firebase Console and add a user to your project. Please be sure to enable email authentication when updating the project in your Firebase Console.
AuthService
getUser
property from the AuthService
at startup to determine which page to load in main.dart
HomePage
to show email address of the logged in FirebaseUser
LoginPage
to call the loginUser
method on the AuthService
to login a user using the Firebase API to see if we can log in a real FirebaseUser
First the authentication service, which is where we are just wrapping some of the basic Firebase functions that we need for authentication and determining if there is already a user persisted from a previous session.
import 'package:firebase_auth/firebase_auth.dart';
import 'dart:async';
import 'package:flutter/cupertino.dart';
class AuthService with ChangeNotifier {
final FirebaseAuth _auth = FirebaseAuth.instance;
///
/// return the Future with firebase user object FirebaseUser if one exists
///
Future getUser() {
return _auth.currentUser();
}
// wrapping the firebase calls
Future logout() async {
var result = FirebaseAuth.instance.signOut();
notifyListeners();
return result;
}
///
/// wrapping the firebase call to signInWithEmailAndPassword
/// `email` String
/// `password` String
///
Future loginUser({String email, String password}) async {
try {
var result = await FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: password);
// since something changed, let's notify the listeners...
notifyListeners();
return result;
} catch (e) {
// throw the Firebase AuthException that we caught
throw new AuthException(e.code, e.message);
}
}
}
Markdium-Dart.dart
As you can see from the code above, we still have the same methods for accessing our AuthService
; the only difference now is that we have replaced the call with real calls to the Firebase back end that you have set up.
Notice we no longer need to keep a property with the current user since Firebase will manage that for us. All we need to do is call the method getUser
, and if there is a user we will get an object. Otherwise it will return null.
Most important to notice is that we are calling notifyListeners()
when the login state is changing during logging in or logging out.
There are no real modifications needed to the file since we are working with the same external API. The only difference is that now we are returning a FirebaseUser
object, so let’s add a specific type to the code, and touch up a few more things.
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: FutureBuilder(
future: Provider.of(context).getUser(),
builder: (context, AsyncSnapshot snapshot) { // ⇐ NEW
if (snapshot.connectionState == ConnectionState.done) {
// log error to console ⇐ NEW
if (snapshot.error != null) {
print("error");
return Text(snapshot.error.toString());
}
// redirect to the proper page, pass the user into the
// `HomePage` so we can display the user email in welcome msg ⇐ NEW
return snapshot.hasData ? HomePage(snapshot.data) : LoginPage();
} else {
// show loading indicator ⇐ NEW
return LoadingCircle();
}
},
),
);
}
}
Markdium-Dart.dart
We have added the object type, FirebaseUser
, associated with the AsyncSnapshot
, and we are now checking for an error in case there is a problem loading Firebase initially.
We have also added a new parameter to the constructor of the HomePage
widget, which is the FirebaseUser
object returned from getUser
call made to the AuthService
. We will see in the next section how the new parameter is used.
Finally, we added a new widget called LoadingCircle
to give us a nice user experience when the application is starting up and accessing Firebase
to check for a new user. See the code below for the LoadingCircle
widget.
See documentation on CircularProgressIndicator
class LoadingCircle extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
child: CircularProgressIndicator(),
alignment: Alignment(0.0, 0.0),
),
);
}
}
Markdium-Dart.dart
home_page.dart
We need to first modify the widget by adding a new constructor that will hold the Firebase user passed in from the FutureBuilder in main.dart
class HomePage extends StatefulWidget {
final FirebaseUser currentUser; // ⇐ NEW
HomePage(this.currentUser); // ⇐ NEW
@override
_HomePageState createState() => _HomePageState();
}
Markdium-Dart.dart
Now we have access to the information on the current user from the widget; we can access it when rendering the HomePage
by make the modifications you see below. We will just add a few more widgets to the build method:
children: [
SizedBox(height: 20.0), // ⇐ NEW
Text( // ⇐ NEW
'Home Page Flutter Firebase Content',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 20.0), // ⇐ NEW
Text( // ⇐ NEW
`Welcome ${widget.currentUser.email}`,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic),
),
SizedBox(height: 20.0),
RaisedButton(
child: Text("LOGOUT"),
onPressed: () async {
await Provider.of(context).logout();
})
],
Markdium-Dart.dart
login_page.dart
Since the API signature hasn’t changed, we need to do very little to this function to get the desired results. However, it would be best to do some better error checking.
With Future
we need to wrap the call with a try
catch
block since any errors that happen with Firebase will be thrown as exceptions. We then will display the error message in a dialog. See code for the method _buildErrorDialog
and the rest of the changes below.
Add the new import for the error exception:
import 'package:firebase_auth/firebase_auth.dart';
Markdium-Dart.dart
Make the appropriate changes to the onPressed
method of the login button.
onPressed: () async {
// save the fields..
final form = _formKey.currentState;
form.save();
// Validate will return true if is valid, or false if invalid.
if (form.validate()) {
try {
FirebaseUser result =
await Provider.of(context).loginUser(
email: _email, password: _password);
print(result);
} on AuthException catch (error) {
// handle the firebase specific error
return _buildErrorDialog(context, error.message);
} on Exception catch (error) {
// gracefully handle anything else that might happen..
return _buildErrorDialog(context, error.toString());
}
}
},
Markdium-Dart.dart
Add the code for the new private _buildErrorDialog
method that will display errors from the call to the AuthService
login method.
Future _buildErrorDialog(BuildContext context, _message) {
return showDialog(
builder: (context) {
return AlertDialog(
title: Text('Error Message'),
content: Text(_message),
actions: [
FlatButton(
child: Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
})
],
);
},
context: context,
);
}
Markdium-Dart.dart
At this point, you should have a functioning application with the basic login flow where the user will be logged into Firebase using the credential for the test user you added in the Firebase Console.
Try entering invalid credentials for the password, and incomplete email addresses, and the errors should be displayed appropriately.
#Flutter #Firebase