In the following tutorial, we’re going to build a Firebase Login Flow in Flutter using the Bloc library.
We’ll start off by creating a brand new Flutter project.
flutter create flutter_firebase_login
Just like in the login tutorial , we’re going to create internal packages to better layer our application architecture and maintain clear boundaries and to maximize both reusability as well as improve testability.
In this case, the firebase_auth and google_sign_in packages are going to be our data layer so we’re only going to be creating an AuthenticationRepository
to compose data from the two API clients.
Authentication Repository
The AuthenticationRepository
will be responsible for abstracting the internal implementation details of how we authenticate and fetch user information. In this case, it will be integrating with Firebase but we can always change the internal implementation later on and our application will be unaffected.
We’ll start by creating packages/authentication_repository
and a pubspec.yaml
at the root of the project.
name : authentication_repository
description : Dart package which manages the authentication domain.
firebase_auth_platform_interface : ^7.0.5
firebase_core_platform_interface : ^5.0.0
plugin_platform_interface : ^2.1.7
Next, we can install the dependencies by running:
in the authentication_repository
directory.
Just like most packages, the authentication_repository
will define it’s API surface via packages/authentication_repository/lib/authentication_repository.dart
export 'src/authentication_repository.dart' ;
export 'src/models/models.dart' ;
Next, let’s take a look at the models.
The User
model will describe a user in the context of the authentication domain. For the purposes of this example, a user will consist of an email
, id
, name
, and photo
.
user.dart
The AuthenticationRepository
is responsible for abstracting the underlying implementation of how a user is authenticated, as well as how a user is fetched.
import 'package:authentication_repository/authentication_repository.dart' ;
import 'package:cache/cache.dart' ;
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:google_sign_in/google_sign_in.dart' ;
import 'package:meta/meta.dart' ;
/// {@template sign_up_with_email_and_password_failure}
/// Thrown during the sign up process if a failure occurs.
class SignUpWithEmailAndPasswordFailure implements Exception {
/// {@macro sign_up_with_email_and_password_failure}
const SignUpWithEmailAndPasswordFailure ([
this .message = 'An unknown exception occurred.' ,
/// Create an authentication message
/// from a firebase authentication exception code.
/// https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/createUserWithEmailAndPassword.html
factory SignUpWithEmailAndPasswordFailure . fromCode ( String code) {
return const SignUpWithEmailAndPasswordFailure (
'Email is not valid or badly formatted.' ,
return const SignUpWithEmailAndPasswordFailure (
'This user has been disabled. Please contact support for help.' ,
case 'email-already-in-use' :
return const SignUpWithEmailAndPasswordFailure (
'An account already exists for that email.' ,
case 'operation-not-allowed' :
return const SignUpWithEmailAndPasswordFailure (
'Operation is not allowed. Please contact support.' ,
return const SignUpWithEmailAndPasswordFailure (
'Please enter a stronger password.' ,
return const SignUpWithEmailAndPasswordFailure ();
/// The associated error message.
/// {@template log_in_with_email_and_password_failure}
/// Thrown during the login process if a failure occurs.
/// https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/signInWithEmailAndPassword.html
class LogInWithEmailAndPasswordFailure implements Exception {
/// {@macro log_in_with_email_and_password_failure}
const LogInWithEmailAndPasswordFailure ([
this .message = 'An unknown exception occurred.' ,
/// Create an authentication message
/// from a firebase authentication exception code.
factory LogInWithEmailAndPasswordFailure . fromCode ( String code) {
return const LogInWithEmailAndPasswordFailure (
'Email is not valid or badly formatted.' ,
return const LogInWithEmailAndPasswordFailure (
'This user has been disabled. Please contact support for help.' ,
return const LogInWithEmailAndPasswordFailure (
'Email is not found, please create an account.' ,
return const LogInWithEmailAndPasswordFailure (
'Incorrect password, please try again.' ,
return const LogInWithEmailAndPasswordFailure ();
/// The associated error message.
/// {@template log_in_with_google_failure}
/// Thrown during the sign in with google process if a failure occurs.
/// https://pub.dev/documentation/firebase_auth/latest/firebase_auth/FirebaseAuth/signInWithCredential.html
class LogInWithGoogleFailure implements Exception {
/// {@macro log_in_with_google_failure}
const LogInWithGoogleFailure ([
this .message = 'An unknown exception occurred.' ,
/// Create an authentication message
/// from a firebase authentication exception code.
factory LogInWithGoogleFailure . fromCode ( String code) {
case 'account-exists-with-different-credential' :
return const LogInWithGoogleFailure (
'Account exists with different credentials.' ,
case 'invalid-credential' :
return const LogInWithGoogleFailure (
'The credential received is malformed or has expired.' ,
case 'operation-not-allowed' :
return const LogInWithGoogleFailure (
'Operation is not allowed. Please contact support.' ,
return const LogInWithGoogleFailure (
'This user has been disabled. Please contact support for help.' ,
return const LogInWithGoogleFailure (
'Email is not found, please create an account.' ,
return const LogInWithGoogleFailure (
'Incorrect password, please try again.' ,
case 'invalid-verification-code' :
return const LogInWithGoogleFailure (
'The credential verification code received is invalid.' ,
case 'invalid-verification-id' :
return const LogInWithGoogleFailure (
'The credential verification ID received is invalid.' ,
return const LogInWithGoogleFailure ();
/// The associated error message.
/// Thrown during the logout process if a failure occurs.
class LogOutFailure implements Exception {}
/// {@template authentication_repository}
/// Repository which manages user authentication.
class AuthenticationRepository {
/// {@macro authentication_repository}
AuthenticationRepository ({
firebase_auth. FirebaseAuth ? firebaseAuth,
GoogleSignIn ? googleSignIn,
}) : _cache = cache ?? CacheClient (),
_firebaseAuth = firebaseAuth ?? firebase_auth. FirebaseAuth .instance,
_googleSignIn = googleSignIn ?? GoogleSignIn . standard ();
final CacheClient _cache;
final firebase_auth. FirebaseAuth _firebaseAuth;
final GoogleSignIn _googleSignIn;
/// Whether or not the current environment is web
/// Should only be overridden for testing purposes. Otherwise,
/// Should only be used for testing purposes.
static const userCacheKey = '__user_cache_key__' ;
/// Stream of [User] which will emit the current user when
/// the authentication state changes.
/// Emits [User.empty] if the user is not authenticated.
return _firebaseAuth. authStateChanges (). map ((firebaseUser) {
final user = firebaseUser == null ? User .empty : firebaseUser.toUser;
_cache. write (key : userCacheKey, value : user);
/// Returns the current cached user.
/// Defaults to [User.empty] if there is no cached user.
return _cache. read < User >(key : userCacheKey) ?? User .empty;
/// Creates a new user with the provided [email] and [password] .
/// Throws a [SignUpWithEmailAndPasswordFailure] if an exception occurs.
Future < void > signUp ({ required String email, required String password}) async {
await _firebaseAuth. createUserWithEmailAndPassword (
} on firebase_auth. FirebaseAuthException catch (e) {
throw SignUpWithEmailAndPasswordFailure . fromCode (e.code);
throw const SignUpWithEmailAndPasswordFailure ();
/// Starts the Sign In with Google Flow.
/// Throws a [LogInWithGoogleFailure] if an exception occurs.
Future < void > logInWithGoogle () async {
late final firebase_auth. AuthCredential credential;
final googleProvider = firebase_auth. GoogleAuthProvider ();
final userCredential = await _firebaseAuth. signInWithPopup (
credential = userCredential.credential ! ;
final googleUser = await _googleSignIn. signIn ();
final googleAuth = await googleUser ! .authentication;
credential = firebase_auth. GoogleAuthProvider . credential (
accessToken : googleAuth.accessToken,
idToken : googleAuth.idToken,
await _firebaseAuth. signInWithCredential (credential);
} on firebase_auth. FirebaseAuthException catch (e) {
throw LogInWithGoogleFailure . fromCode (e.code);
throw const LogInWithGoogleFailure ();
/// Signs in with the provided [email] and [password] .
/// Throws a [LogInWithEmailAndPasswordFailure] if an exception occurs.
Future < void > logInWithEmailAndPassword ({
required String password,
await _firebaseAuth. signInWithEmailAndPassword (
} on firebase_auth. FirebaseAuthException catch (e) {
throw LogInWithEmailAndPasswordFailure . fromCode (e.code);
throw const LogInWithEmailAndPasswordFailure ();
/// Signs out the current user which will emit
/// [User.empty] from the [user] Stream.
/// Throws a [LogOutFailure] if an exception occurs.
Future < void > logOut () async {
extension on firebase_auth. User {
/// Maps a [firebase_auth.User] into a [User] .
return User (id : uid, email : email, name : displayName, photo : photoURL);
The AuthenticationRepository
exposes a Stream<User>
which we can subscribe to in order to be notified of when a User
changes. In addition, it exposes methods to signUp
, logInWithGoogle
, logInWithEmailAndPassword
, and logOut
.
That’s it for the AuthenticationRepository
. Next, let’s take a look at how to integrate it into the Flutter project we created.
We need to follow the firebase_auth usage instructions in order to hook up our application to Firebase and enable google_sign_in .
We can replace the generated pubspec.yaml
at the root of the project with the following:
name : flutter_firebase_login
description : A new Flutter project.
authentication_repository :
path : packages/authentication_repository
font_awesome_flutter : ^10.1.0
path : packages/form_inputs
uses-material-design : true
Notice that we are specifying an assets directory for all of our applications local assets. Create an assets
directory in the root of your project and add the bloc logo asset (which we’ll use later).
Then install all of the dependencies:
The main.dart
file can be replaced with the following:
import 'package:authentication_repository/authentication_repository.dart' ;
import 'package:bloc/bloc.dart' ;
import 'package:firebase_core/firebase_core.dart' ;
import 'package:flutter/widgets.dart' ;
import 'package:flutter_firebase_login/app/app.dart' ;
Future < void > main () async {
WidgetsFlutterBinding . ensureInitialized ();
Bloc .observer = const AppBlocObserver ();
await Firebase . initializeApp ();
final authenticationRepository = AuthenticationRepository ();
await authenticationRepository.user.first;
runApp ( App (authenticationRepository : authenticationRepository));
It’s simply setting up some global configuration for the application and calling runApp
with an instance of App
.
Just like in the login tutorial , our app.dart
will provide an instance of the AuthenticationRepository
to the application via RepositoryProvider
and also creates and provides an instance of AuthenticationBloc
. Then AppView
consumes the AuthenticationBloc
and handles updating the current route based on the AuthenticationState
.
import 'package:authentication_repository/authentication_repository.dart' ;
import 'package:flow_builder/flow_builder.dart' ;
import 'package:flutter/material.dart' ;
import 'package:flutter_bloc/flutter_bloc.dart' ;
import 'package:flutter_firebase_login/app/app.dart' ;
import 'package:flutter_firebase_login/theme.dart' ;
class App extends StatelessWidget {
required AuthenticationRepository authenticationRepository,
}) : _authenticationRepository = authenticationRepository;
final AuthenticationRepository _authenticationRepository;
Widget build ( BuildContext context) {
return RepositoryProvider . value (
value : _authenticationRepository,
authenticationRepository : _authenticationRepository,
).. add ( const AppUserSubscriptionRequested ()),
class AppView extends StatelessWidget {
const AppView ({ super .key});
Widget build ( BuildContext context) {
home : FlowBuilder < AppStatus >(
state : context. select (( AppBloc bloc) => bloc.state.status),
onGeneratePages : onGenerateAppViewPages,
The AppBloc
is responsible for managing the global state of the application. It has a dependency on the AuthenticationRepository
and subscribes to the user
Stream in order to emit new states in response to changes in the current user.
The AppState
consists of an AppStatus
and a User
. The default constructor accepts an optional User
and redirects to the private constructor with the appropriate authentication status.
enum AppStatus { authenticated, unauthenticated }
final class AppState extends Equatable {
const AppState ({ User user = User .empty})
status : user == User .empty
? AppStatus .unauthenticated
: AppStatus .authenticated,
const AppState ._({ required this .status, this .user = User .empty});
List < Object > get props => [status, user];
The AppEvent
has two subclasses:
AppUserSubscriptionRequested
which notifies the bloc to subscribe to the user stream.
AppLogoutPressed
which notifies the bloc of a user logout action.
final class AppUserSubscriptionRequested extends AppEvent {
const AppUserSubscriptionRequested ();
final class AppLogoutPressed extends AppEvent {
const AppLogoutPressed ();
In the constructor body, AppEvent
subclasses are mapped to their corresponding event handlers.
In the _onUserSubscriptionRequested
event handler, the AppBloc
uses emit.onEach
to subscribe to the user stream of the AuthenticationRepository
and emit a state in response to each User
.
emit.onEach
creates a stream subscription internally and takes care of canceling it when either AppBloc
or the user stream is closed.
If the user stream emits an error, addError
forwards the error and stack trace to any BlocObserver
listening.
Caution
If onError
is omitted, any errors on the user stream are considered unhandled, and will be thrown by onEach
. As a result, the subscription to the user stream will be canceled.
import 'package:authentication_repository/authentication_repository.dart' ;
import 'package:bloc/bloc.dart' ;
import 'package:equatable/equatable.dart' ;
class AppBloc extends Bloc < AppEvent , AppState > {
AppBloc ({ required AuthenticationRepository authenticationRepository})
: _authenticationRepository = authenticationRepository,
super ( AppState (user : authenticationRepository.currentUser)) {
on < AppUserSubscriptionRequested > (_onUserSubscriptionRequested);
on < AppLogoutPressed > (_onLogoutPressed);
final AuthenticationRepository _authenticationRepository;
Future < void > _onUserSubscriptionRequested (
AppUserSubscriptionRequested event,
_authenticationRepository.user,
onData : (user) => emit ( AppState (user : user)),
_authenticationRepository. logOut ();
An Email
and Password
input model are useful for encapsulating the validation logic and will be used in both the LoginForm
and SignUpForm
(later in the tutorial).
Both input models are made using the formz package and allow us to work with a validated object rather than a primitive type like a String
.
import 'package:formz/formz.dart' ;
/// Validation errors for the [Email] [FormzInput] .
enum EmailValidationError {
/// Generic invalid error.
/// Form input for an email input.
class Email extends FormzInput < String , EmailValidationError > {
const Email . pure () : super . pure ( '' );
const Email . dirty ([ super .value = '' ]) : super . dirty ();
static final RegExp _emailRegExp = RegExp (
r'^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' ,
EmailValidationError ? validator ( String ? value) {
return _emailRegExp. hasMatch (value ?? '' )
: EmailValidationError .invalid;
import 'package:formz/formz.dart' ;
/// Validation errors for the [Password] [FormzInput] .
enum PasswordValidationError {
/// Generic invalid error.
/// Form input for an password input.
class Password extends FormzInput < String , PasswordValidationError > {
const Password . pure () : super . pure ( '' );
const Password . dirty ([ super .value = '' ]) : super . dirty ();
static final _passwordRegExp =
RegExp ( r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$' );
PasswordValidationError ? validator ( String ? value) {
return _passwordRegExp. hasMatch (value ?? '' )
: PasswordValidationError .invalid;
The LoginPage
is responsible for creating and providing an instance of LoginCubit
to the LoginForm
.
import 'package:authentication_repository/authentication_repository.dart' ;
import 'package:flutter/material.dart' ;
import 'package:flutter_bloc/flutter_bloc.dart' ;
import 'package:flutter_firebase_login/login/login.dart' ;
class LoginPage extends StatelessWidget {
const LoginPage ({ super .key});
static Page < void > page () => const MaterialPage < void >(child : LoginPage ());
Widget build ( BuildContext context) {
appBar : AppBar (title : const Text ( 'Login' )),
padding : const EdgeInsets . all ( 8 ),
create : (_) => LoginCubit (context. read < AuthenticationRepository >()),
child : const LoginForm (),
The LoginCubit
is responsible for managing the LoginState
of the form. It exposes APIs to logInWithCredentials
, logInWithGoogle
, as well as gets notified when the email/password are updated.
The LoginState
consists of an Email
, Password
, and FormzStatus
. The Email
and Password
models extend FormzInput
from the formz package.
part of 'login_cubit.dart' ;
final class LoginState extends Equatable {
this .email = const Email . pure (),
this .password = const Password . pure (),
this .status = FormzSubmissionStatus .initial,
final FormzSubmissionStatus status;
final String ? errorMessage;
List < Object ?> get props => [email, password, status, isValid, errorMessage];
FormzSubmissionStatus ? status,
email : email ?? this .email,
password : password ?? this .password,
status : status ?? this .status,
isValid : isValid ?? this .isValid,
errorMessage : errorMessage ?? this .errorMessage,
The LoginCubit
has a dependency on the AuthenticationRepository
in order to sign the user in either via credentials or via google sign in.
import 'package:authentication_repository/authentication_repository.dart' ;
import 'package:bloc/bloc.dart' ;
import 'package:equatable/equatable.dart' ;
import 'package:form_inputs/form_inputs.dart' ;
import 'package:formz/formz.dart' ;
class LoginCubit extends Cubit < LoginState > {
LoginCubit ( this ._authenticationRepository) : super ( const LoginState ());
final AuthenticationRepository _authenticationRepository;
void emailChanged ( String value) {
final email = Email . dirty (value);
isValid : Formz . validate ([email, state.password]),
void passwordChanged ( String value) {
final password = Password . dirty (value);
isValid : Formz . validate ([state.email, password]),
Future < void > logInWithCredentials () async {
if ( ! state.isValid) return ;
emit (state. copyWith (status : FormzSubmissionStatus .inProgress));
await _authenticationRepository. logInWithEmailAndPassword (
email : state.email.value,
password : state.password.value,
emit (state. copyWith (status : FormzSubmissionStatus .success));
} on LogInWithEmailAndPasswordFailure catch (e) {
status : FormzSubmissionStatus .failure,
emit (state. copyWith (status : FormzSubmissionStatus .failure));
Future < void > logInWithGoogle () async {
emit (state. copyWith (status : FormzSubmissionStatus .inProgress));
await _authenticationRepository. logInWithGoogle ();
emit (state. copyWith (status : FormzSubmissionStatus .success));
} on LogInWithGoogleFailure catch (e) {
status : FormzSubmissionStatus .failure,
emit (state. copyWith (status : FormzSubmissionStatus .failure));
The LoginForm
is responsible for rendering the form in response to the LoginState
and invokes methods on the LoginCubit
in response to user interactions.
import 'package:flutter/material.dart' ;
import 'package:flutter_bloc/flutter_bloc.dart' ;
import 'package:flutter_firebase_login/login/login.dart' ;
import 'package:flutter_firebase_login/sign_up/sign_up.dart' ;
import 'package:font_awesome_flutter/font_awesome_flutter.dart' ;
import 'package:formz/formz.dart' ;
class LoginForm extends StatelessWidget {
const LoginForm ({ super .key});
Widget build ( BuildContext context) {
return BlocListener < LoginCubit , LoginState >(
listener : (context, state) {
if (state.status.isFailure) {
ScaffoldMessenger . of (context)
content : Text (state.errorMessage ?? 'Authentication Failure' ),
alignment : const Alignment ( 0 , - 1 / 3 ),
child : SingleChildScrollView (
mainAxisSize : MainAxisSize .min,
'assets/bloc_logo_small.png' ,
const SizedBox (height : 16 ),
const SizedBox (height : 8 ),
const SizedBox (height : 8 ),
const SizedBox (height : 8 ),
const SizedBox (height : 4 ),
class _EmailInput extends StatelessWidget {
Widget build ( BuildContext context) {
final displayError = context. select (
( LoginCubit cubit) => cubit.state.email.displayError,
key : const Key ( 'loginForm_emailInput_textField' ),
onChanged : (email) => context. read < LoginCubit >(). emailChanged (email),
keyboardType : TextInputType .emailAddress,
decoration : InputDecoration (
errorText : displayError != null ? 'invalid email' : null ,
class _PasswordInput extends StatelessWidget {
Widget build ( BuildContext context) {
final displayError = context. select (
( LoginCubit cubit) => cubit.state.password.displayError,
key : const Key ( 'loginForm_passwordInput_textField' ),
context. read < LoginCubit >(). passwordChanged (password),
decoration : InputDecoration (
errorText : displayError != null ? 'invalid password' : null ,
class _LoginButton extends StatelessWidget {
Widget build ( BuildContext context) {
final isInProgress = context. select (
( LoginCubit cubit) => cubit.state.status.isInProgress,
if (isInProgress) return const CircularProgressIndicator ();
final isValid = context. select (
( LoginCubit cubit) => cubit.state.isValid,
key : const Key ( 'loginForm_continue_raisedButton' ),
style : ElevatedButton . styleFrom (
shape : RoundedRectangleBorder (
borderRadius : BorderRadius . circular ( 30 ),
backgroundColor : const Color ( 0xFFFFD600 ),
? () => context. read < LoginCubit >(). logInWithCredentials ()
child : const Text ( 'LOGIN' ),
class _GoogleLoginButton extends StatelessWidget {
Widget build ( BuildContext context) {
final theme = Theme . of (context);
return ElevatedButton . icon (
key : const Key ( 'loginForm_googleLogin_raisedButton' ),
style : TextStyle (color : Colors .white),
style : ElevatedButton . styleFrom (
shape : RoundedRectangleBorder (
borderRadius : BorderRadius . circular ( 30 ),
backgroundColor : theme.colorScheme.secondary,
icon : const Icon ( FontAwesomeIcons .google, color : Colors .white),
onPressed : () => context. read < LoginCubit >(). logInWithGoogle (),
class _SignUpButton extends StatelessWidget {
Widget build ( BuildContext context) {
final theme = Theme . of (context);
key : const Key ( 'loginForm_createAccount_flatButton' ),
onPressed : () => Navigator . of (context). push < void >( SignUpPage . route ()),
style : TextStyle (color : theme.primaryColor),
The LoginForm
also renders a “Create Account” button which navigates to the SignUpPage
where a user can create a brand new account.
The SignUp
structure mirrors the Login
structure and consists of a SignUpPage
, SignUpView
, and SignUpCubit
.
The SignUpPage
is just responsible for creating and providing an instance of the SignUpCubit
to the SignUpForm
(exactly like in LoginPage
).
import 'package:authentication_repository/authentication_repository.dart' ;
import 'package:flutter/material.dart' ;
import 'package:flutter_bloc/flutter_bloc.dart' ;
import 'package:flutter_firebase_login/sign_up/sign_up.dart' ;
class SignUpPage extends StatelessWidget {
const SignUpPage ({ super .key});
static Route < void > route () {
return MaterialPageRoute < void >(builder : (_) => const SignUpPage ());
Widget build ( BuildContext context) {
appBar : AppBar (title : const Text ( 'Sign Up' )),
padding : const EdgeInsets . all ( 8 ),
child : BlocProvider < SignUpCubit >(
create : (_) => SignUpCubit (context. read < AuthenticationRepository >()),
child : const SignUpForm (),
The SignUpCubit
manages the state of the SignUpForm
and communicates with the AuthenticationRepository
in order to create new user accounts.
The SignUpState
reuses the same Email
and Password
form input models because the validation logic is the same.
part of 'sign_up_cubit.dart' ;
final class SignUpState extends Equatable {
this .email = const Email . pure (),
this .password = const Password . pure (),
this .confirmedPassword = const ConfirmedPassword . pure (),
this .status = FormzSubmissionStatus .initial,
final ConfirmedPassword confirmedPassword;
final FormzSubmissionStatus status;
final String ? errorMessage;
List < Object ?> get props => [
ConfirmedPassword ? confirmedPassword,
FormzSubmissionStatus ? status,
email : email ?? this .email,
password : password ?? this .password,
confirmedPassword : confirmedPassword ?? this .confirmedPassword,
status : status ?? this .status,
isValid : isValid ?? this .isValid,
errorMessage : errorMessage ?? this .errorMessage,
The SignUpCubit
is extremely similar to the LoginCubit
with the main exception being it exposes an API to submit the form as opposed to login.
import 'package:authentication_repository/authentication_repository.dart' ;
import 'package:bloc/bloc.dart' ;
import 'package:equatable/equatable.dart' ;
import 'package:form_inputs/form_inputs.dart' ;
import 'package:formz/formz.dart' ;
part 'sign_up_state.dart' ;
class SignUpCubit extends Cubit < SignUpState > {
SignUpCubit ( this ._authenticationRepository) : super ( const SignUpState ());
final AuthenticationRepository _authenticationRepository;
void emailChanged ( String value) {
final email = Email . dirty (value);
isValid : Formz . validate ([
void passwordChanged ( String value) {
final password = Password . dirty (value);
final confirmedPassword = ConfirmedPassword . dirty (
password : password.value,
value : state.confirmedPassword.value,
confirmedPassword : confirmedPassword,
isValid : Formz . validate ([
void confirmedPasswordChanged ( String value) {
final confirmedPassword = ConfirmedPassword . dirty (
password : state.password.value,
confirmedPassword : confirmedPassword,
isValid : Formz . validate ([
Future < void > signUpFormSubmitted () async {
if ( ! state.isValid) return ;
emit (state. copyWith (status : FormzSubmissionStatus .inProgress));
await _authenticationRepository. signUp (
email : state.email.value,
password : state.password.value,
emit (state. copyWith (status : FormzSubmissionStatus .success));
} on SignUpWithEmailAndPasswordFailure catch (e) {
status : FormzSubmissionStatus .failure,
emit (state. copyWith (status : FormzSubmissionStatus .failure));
The SignUpForm
is responsible for rendering the form in response to the SignUpState
and invokes methods on the SignUpCubit
in response to user interactions.
import 'package:flutter/material.dart' ;
import 'package:flutter_bloc/flutter_bloc.dart' ;
import 'package:flutter_firebase_login/sign_up/sign_up.dart' ;
import 'package:formz/formz.dart' ;
class SignUpForm extends StatelessWidget {
const SignUpForm ({ super .key});
Widget build ( BuildContext context) {
return BlocListener < SignUpCubit , SignUpState >(
listener : (context, state) {
if (state.status.isSuccess) {
Navigator . of (context). pop ();
} else if (state.status.isFailure) {
ScaffoldMessenger . of (context)
SnackBar (content : Text (state.errorMessage ?? 'Sign Up Failure' )),
alignment : const Alignment ( 0 , - 1 / 3 ),
mainAxisSize : MainAxisSize .min,
const SizedBox (height : 8 ),
const SizedBox (height : 8 ),
const SizedBox (height : 8 ),
class _EmailInput extends StatelessWidget {
Widget build ( BuildContext context) {
final displayError = context. select (
( SignUpCubit cubit) => cubit.state.email.displayError,
key : const Key ( 'signUpForm_emailInput_textField' ),
onChanged : (email) => context. read < SignUpCubit >(). emailChanged (email),
keyboardType : TextInputType .emailAddress,
decoration : InputDecoration (
errorText : displayError != null ? 'invalid email' : null ,
class _PasswordInput extends StatelessWidget {
Widget build ( BuildContext context) {
final displayError = context. select (
( SignUpCubit cubit) => cubit.state.password.displayError,
key : const Key ( 'signUpForm_passwordInput_textField' ),
context. read < SignUpCubit >(). passwordChanged (password),
decoration : InputDecoration (
errorText : displayError != null ? 'invalid password' : null ,
class _ConfirmPasswordInput extends StatelessWidget {
Widget build ( BuildContext context) {
final displayError = context. select (
( SignUpCubit cubit) => cubit.state.confirmedPassword.displayError,
key : const Key ( 'signUpForm_confirmedPasswordInput_textField' ),
onChanged : (confirmPassword) =>
context. read < SignUpCubit >(). confirmedPasswordChanged (confirmPassword),
decoration : InputDecoration (
labelText : 'confirm password' ,
errorText : displayError != null ? 'passwords do not match' : null ,
class _SignUpButton extends StatelessWidget {
Widget build ( BuildContext context) {
final isInProgress = context. select (
( SignUpCubit cubit) => cubit.state.status.isInProgress,
if (isInProgress) return const CircularProgressIndicator ();
final isValid = context. select (
( SignUpCubit cubit) => cubit.state.isValid,
key : const Key ( 'signUpForm_continue_raisedButton' ),
style : ElevatedButton . styleFrom (
shape : RoundedRectangleBorder (
borderRadius : BorderRadius . circular ( 30 ),
backgroundColor : Colors .orangeAccent,
? () => context. read < SignUpCubit >(). signUpFormSubmitted ()
child : const Text ( 'SIGN UP' ),
After a user either successfully logs in or signs up, the user
stream will be updated which will trigger a state change in the AuthenticationBloc
and will result in the AppView
pushing the HomePage
route onto the navigation stack.
From the HomePage
, the user can view their profile information and log out by tapping the exit icon in the AppBar
.
import 'package:flutter/material.dart' ;
import 'package:flutter_bloc/flutter_bloc.dart' ;
import 'package:flutter_firebase_login/app/app.dart' ;
import 'package:flutter_firebase_login/home/home.dart' ;
class HomePage extends StatelessWidget {
const HomePage ({ super .key});
static Page < void > page () => const MaterialPage < void >(child : HomePage ());
Widget build ( BuildContext context) {
final textTheme = Theme . of (context).textTheme;
final user = context. select (( AppBloc bloc) => bloc.state.user);
title : const Text ( 'Home' ),
key : const Key ( 'homePage_logout_iconButton' ),
icon : const Icon ( Icons .exit_to_app),
context. read < AppBloc >(). add ( const AppLogoutPressed ());
alignment : const Alignment ( 0 , - 1 / 3 ),
mainAxisSize : MainAxisSize .min,
Avatar (photo : user.photo),
const SizedBox (height : 4 ),
Text (user.email ?? '' , style : textTheme.titleLarge),
const SizedBox (height : 4 ),
Text (user.name ?? '' , style : textTheme.headlineSmall),
At this point we have a pretty solid login implementation using Firebase and we have decoupled our presentation layer from the business logic layer by using the Bloc Library.
The full source for this example can be found here .