Skip to content

Flutter Firebase Login

advanced

In the following tutorial, we’re going to build a Firebase Login Flow in Flutter using the Bloc library.

demo

Key Topics

Setup

We’ll start off by creating a brand new Flutter project.

Terminal window
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.

Setup

We’ll start by creating packages/authentication_repository and a pubspec.yaml at the root of the project.

packages/authentication_repository/pubspec.yaml
name: authentication_repository
description: Dart package which manages the authentication domain.
version: 1.0.0
publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
dependencies:
cache:
path: ../cache
equatable: ^2.0.5
firebase_auth: ^5.0.0
firebase_core: ^3.0.0
flutter:
sdk: flutter
google_sign_in: ^6.1.0
meta: ^1.8.0
dev_dependencies:
firebase_auth_platform_interface: ^7.0.5
firebase_core_platform_interface: ^5.0.0
flutter_test:
sdk: flutter
mocktail: ^1.0.0
plugin_platform_interface: ^2.1.7

Next, we can install the dependencies by running:

Terminal window
flutter packages get

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

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.

User

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

Repository

The AuthenticationRepository is responsible for abstracting the underlying implementation of how a user is authenticated, as well as how a user is fetched.

packages/authentication_repository/lib/src/authentication_repository.dart
import 'dart:async';
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.
/// {@endtemplate}
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) {
switch (code) {
case 'invalid-email':
return const SignUpWithEmailAndPasswordFailure(
'Email is not valid or badly formatted.',
);
case 'user-disabled':
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.',
);
case 'weak-password':
return const SignUpWithEmailAndPasswordFailure(
'Please enter a stronger password.',
);
default:
return const SignUpWithEmailAndPasswordFailure();
}
}
/// The associated error message.
final String 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
/// {@endtemplate}
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) {
switch (code) {
case 'invalid-email':
return const LogInWithEmailAndPasswordFailure(
'Email is not valid or badly formatted.',
);
case 'user-disabled':
return const LogInWithEmailAndPasswordFailure(
'This user has been disabled. Please contact support for help.',
);
case 'user-not-found':
return const LogInWithEmailAndPasswordFailure(
'Email is not found, please create an account.',
);
case 'wrong-password':
return const LogInWithEmailAndPasswordFailure(
'Incorrect password, please try again.',
);
default:
return const LogInWithEmailAndPasswordFailure();
}
}
/// The associated error message.
final String 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
/// {@endtemplate}
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) {
switch (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.',
);
case 'user-disabled':
return const LogInWithGoogleFailure(
'This user has been disabled. Please contact support for help.',
);
case 'user-not-found':
return const LogInWithGoogleFailure(
'Email is not found, please create an account.',
);
case 'wrong-password':
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.',
);
default:
return const LogInWithGoogleFailure();
}
}
/// The associated error message.
final String message;
}
/// Thrown during the logout process if a failure occurs.
class LogOutFailure implements Exception {}
/// {@template authentication_repository}
/// Repository which manages user authentication.
/// {@endtemplate}
class AuthenticationRepository {
/// {@macro authentication_repository}
AuthenticationRepository({
CacheClient? cache,
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,
/// defaults to [kIsWeb]
@visibleForTesting
bool isWeb = kIsWeb;
/// User cache key.
/// Should only be used for testing purposes.
@visibleForTesting
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.
Stream<User> get user {
return _firebaseAuth.authStateChanges().map((firebaseUser) {
final user = firebaseUser == null ? User.empty : firebaseUser.toUser;
_cache.write(key: userCacheKey, value: user);
return user;
});
}
/// Returns the current cached user.
/// Defaults to [User.empty] if there is no cached user.
User get currentUser {
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 {
try {
await _firebaseAuth.createUserWithEmailAndPassword(
email: email,
password: password,
);
} on firebase_auth.FirebaseAuthException catch (e) {
throw SignUpWithEmailAndPasswordFailure.fromCode(e.code);
} catch (_) {
throw const SignUpWithEmailAndPasswordFailure();
}
}
/// Starts the Sign In with Google Flow.
///
/// Throws a [LogInWithGoogleFailure] if an exception occurs.
Future<void> logInWithGoogle() async {
try {
late final firebase_auth.AuthCredential credential;
if (isWeb) {
final googleProvider = firebase_auth.GoogleAuthProvider();
final userCredential = await _firebaseAuth.signInWithPopup(
googleProvider,
);
credential = userCredential.credential!;
} else {
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);
} catch (_) {
throw const LogInWithGoogleFailure();
}
}
/// Signs in with the provided [email] and [password].
///
/// Throws a [LogInWithEmailAndPasswordFailure] if an exception occurs.
Future<void> logInWithEmailAndPassword({
required String email,
required String password,
}) async {
try {
await _firebaseAuth.signInWithEmailAndPassword(
email: email,
password: password,
);
} on firebase_auth.FirebaseAuthException catch (e) {
throw LogInWithEmailAndPasswordFailure.fromCode(e.code);
} catch (_) {
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 {
try {
await Future.wait([
_firebaseAuth.signOut(),
_googleSignIn.signOut(),
]);
} catch (_) {
throw LogOutFailure();
}
}
}
extension on firebase_auth.User {
/// Maps a [firebase_auth.User] into a [User].
User get toUser {
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.

Firebase Setup

We need to follow the firebase_auth usage instructions in order to hook up our application to Firebase and enable google_sign_in.

Project Dependencies

We can replace the generated pubspec.yaml at the root of the project with the following:

pubspec.yaml
name: flutter_firebase_login
description: A new Flutter project.
version: 1.0.0+1
publish_to: none
environment:
sdk: ">=3.0.0 <4.0.0"
dependencies:
authentication_repository:
path: packages/authentication_repository
bloc: ^8.1.0
equatable: ^2.0.3
firebase_core: ^3.0.0
flow_builder: ^0.1.0
flutter:
sdk: flutter
flutter_bloc: ^8.1.1
font_awesome_flutter: ^10.1.0
form_inputs:
path: packages/form_inputs
formz: ^0.7.0
google_fonts: ^6.2.1
meta: ^1.7.0
dev_dependencies:
bloc_test: ^9.0.0
flutter_test:
sdk: flutter
mocktail: ^1.0.0
flutter:
uses-material-design: true
assets:
- assets/

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:

Terminal window
flutter packages get

main.dart

The main.dart file can be replaced with the following:

lib/main.dart
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.

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.

lib/app/view/app.dart
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 {
const App({
required AuthenticationRepository authenticationRepository,
super.key,
}) : _authenticationRepository = authenticationRepository;
final AuthenticationRepository _authenticationRepository;
@override
Widget build(BuildContext context) {
return RepositoryProvider.value(
value: _authenticationRepository,
child: BlocProvider(
lazy: false,
create: (_) => AppBloc(
authenticationRepository: _authenticationRepository,
)..add(const AppUserSubscriptionRequested()),
child: const AppView(),
),
);
}
}
class AppView extends StatelessWidget {
const AppView({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: theme,
home: FlowBuilder<AppStatus>(
state: context.select((AppBloc bloc) => bloc.state.status),
onGeneratePages: onGenerateAppViewPages,
),
);
}
}

App Bloc

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.

State

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.

lib/app/bloc/app_state.dart
part of 'app_bloc.dart';
enum AppStatus { authenticated, unauthenticated }
final class AppState extends Equatable {
const AppState({User user = User.empty})
: this._(
status: user == User.empty
? AppStatus.unauthenticated
: AppStatus.authenticated,
user: user,
);
const AppState._({required this.status, this.user = User.empty});
final AppStatus status;
final User user;
@override
List<Object> get props => [status, user];
}

Event

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.
lib/app/bloc/app_event.dart
part of 'app_bloc.dart';
sealed class AppEvent {
const AppEvent();
}
final class AppUserSubscriptionRequested extends AppEvent {
const AppUserSubscriptionRequested();
}
final class AppLogoutPressed extends AppEvent {
const AppLogoutPressed();
}

Bloc

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.

lib/app/bloc/app_bloc.dart
import 'dart:async';
import 'package:authentication_repository/authentication_repository.dart';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'app_event.dart';
part 'app_state.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,
Emitter<AppState> emit,
) {
return emit.onEach(
_authenticationRepository.user,
onData: (user) => emit(AppState(user: user)),
onError: addError,
);
}
void _onLogoutPressed(
AppLogoutPressed event,
Emitter<AppState> emit,
) {
_authenticationRepository.logOut();
}
}

Models

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.

Email

packages/form_inputs/lib/src/email.dart
import 'package:formz/formz.dart';
/// Validation errors for the [Email] [FormzInput].
enum EmailValidationError {
/// Generic invalid error.
invalid
}
/// {@template email}
/// Form input for an email input.
/// {@endtemplate}
class Email extends FormzInput<String, EmailValidationError> {
/// {@macro email}
const Email.pure() : super.pure('');
/// {@macro email}
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-]+)*$',
);
@override
EmailValidationError? validator(String? value) {
return _emailRegExp.hasMatch(value ?? '')
? null
: EmailValidationError.invalid;
}
}

Password

packages/form_inputs/lib/src/password.dart
import 'package:formz/formz.dart';
/// Validation errors for the [Password] [FormzInput].
enum PasswordValidationError {
/// Generic invalid error.
invalid
}
/// {@template password}
/// Form input for an password input.
/// {@endtemplate}
class Password extends FormzInput<String, PasswordValidationError> {
/// {@macro password}
const Password.pure() : super.pure('');
/// {@macro password}
const Password.dirty([super.value = '']) : super.dirty();
static final _passwordRegExp =
RegExp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$');
@override
PasswordValidationError? validator(String? value) {
return _passwordRegExp.hasMatch(value ?? '')
? null
: PasswordValidationError.invalid;
}
}

Login Page

The LoginPage is responsible for creating and providing an instance of LoginCubit to the LoginForm.

lib/login/view/login_page.dart
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());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Padding(
padding: const EdgeInsets.all(8),
child: BlocProvider(
create: (_) => LoginCubit(context.read<AuthenticationRepository>()),
child: const LoginForm(),
),
),
);
}
}

Login Cubit

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.

State

The LoginState consists of an Email, Password, and FormzStatus. The Email and Password models extend FormzInput from the formz package.

lib/login/cubit/login_state.dart
part of 'login_cubit.dart';
final class LoginState extends Equatable {
const LoginState({
this.email = const Email.pure(),
this.password = const Password.pure(),
this.status = FormzSubmissionStatus.initial,
this.isValid = false,
this.errorMessage,
});
final Email email;
final Password password;
final FormzSubmissionStatus status;
final bool isValid;
final String? errorMessage;
@override
List<Object?> get props => [email, password, status, isValid, errorMessage];
LoginState copyWith({
Email? email,
Password? password,
FormzSubmissionStatus? status,
bool? isValid,
String? errorMessage,
}) {
return LoginState(
email: email ?? this.email,
password: password ?? this.password,
status: status ?? this.status,
isValid: isValid ?? this.isValid,
errorMessage: errorMessage ?? this.errorMessage,
);
}
}

Cubit

The LoginCubit has a dependency on the AuthenticationRepository in order to sign the user in either via credentials or via google sign in.

lib/login/cubit/login_cubit.dart
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 'login_state.dart';
class LoginCubit extends Cubit<LoginState> {
LoginCubit(this._authenticationRepository) : super(const LoginState());
final AuthenticationRepository _authenticationRepository;
void emailChanged(String value) {
final email = Email.dirty(value);
emit(
state.copyWith(
email: email,
isValid: Formz.validate([email, state.password]),
),
);
}
void passwordChanged(String value) {
final password = Password.dirty(value);
emit(
state.copyWith(
password: password,
isValid: Formz.validate([state.email, password]),
),
);
}
Future<void> logInWithCredentials() async {
if (!state.isValid) return;
emit(state.copyWith(status: FormzSubmissionStatus.inProgress));
try {
await _authenticationRepository.logInWithEmailAndPassword(
email: state.email.value,
password: state.password.value,
);
emit(state.copyWith(status: FormzSubmissionStatus.success));
} on LogInWithEmailAndPasswordFailure catch (e) {
emit(
state.copyWith(
errorMessage: e.message,
status: FormzSubmissionStatus.failure,
),
);
} catch (_) {
emit(state.copyWith(status: FormzSubmissionStatus.failure));
}
}
Future<void> logInWithGoogle() async {
emit(state.copyWith(status: FormzSubmissionStatus.inProgress));
try {
await _authenticationRepository.logInWithGoogle();
emit(state.copyWith(status: FormzSubmissionStatus.success));
} on LogInWithGoogleFailure catch (e) {
emit(
state.copyWith(
errorMessage: e.message,
status: FormzSubmissionStatus.failure,
),
);
} catch (_) {
emit(state.copyWith(status: FormzSubmissionStatus.failure));
}
}
}

Login Form

The LoginForm is responsible for rendering the form in response to the LoginState and invokes methods on the LoginCubit in response to user interactions.

lib/login/view/login_form.dart
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});
@override
Widget build(BuildContext context) {
return BlocListener<LoginCubit, LoginState>(
listener: (context, state) {
if (state.status.isFailure) {
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(
content: Text(state.errorMessage ?? 'Authentication Failure'),
),
);
}
},
child: Align(
alignment: const Alignment(0, -1 / 3),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/bloc_logo_small.png',
height: 120,
),
const SizedBox(height: 16),
_EmailInput(),
const SizedBox(height: 8),
_PasswordInput(),
const SizedBox(height: 8),
_LoginButton(),
const SizedBox(height: 8),
_GoogleLoginButton(),
const SizedBox(height: 4),
_SignUpButton(),
],
),
),
),
);
}
}
class _EmailInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
final displayError = context.select(
(LoginCubit cubit) => cubit.state.email.displayError,
);
return TextField(
key: const Key('loginForm_emailInput_textField'),
onChanged: (email) => context.read<LoginCubit>().emailChanged(email),
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'email',
helperText: '',
errorText: displayError != null ? 'invalid email' : null,
),
);
}
}
class _PasswordInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
final displayError = context.select(
(LoginCubit cubit) => cubit.state.password.displayError,
);
return TextField(
key: const Key('loginForm_passwordInput_textField'),
onChanged: (password) =>
context.read<LoginCubit>().passwordChanged(password),
obscureText: true,
decoration: InputDecoration(
labelText: 'password',
helperText: '',
errorText: displayError != null ? 'invalid password' : null,
),
);
}
}
class _LoginButton extends StatelessWidget {
@override
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,
);
return ElevatedButton(
key: const Key('loginForm_continue_raisedButton'),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
backgroundColor: const Color(0xFFFFD600),
),
onPressed: isValid
? () => context.read<LoginCubit>().logInWithCredentials()
: null,
child: const Text('LOGIN'),
);
}
}
class _GoogleLoginButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ElevatedButton.icon(
key: const Key('loginForm_googleLogin_raisedButton'),
label: const Text(
'SIGN IN WITH GOOGLE',
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 {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return TextButton(
key: const Key('loginForm_createAccount_flatButton'),
onPressed: () => Navigator.of(context).push<void>(SignUpPage.route()),
child: Text(
'CREATE ACCOUNT',
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.

Sign Up Page

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).

lib/sign_up/view/sign_up_page.dart
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());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sign Up')),
body: Padding(
padding: const EdgeInsets.all(8),
child: BlocProvider<SignUpCubit>(
create: (_) => SignUpCubit(context.read<AuthenticationRepository>()),
child: const SignUpForm(),
),
),
);
}
}

Sign Up Cubit

The SignUpCubit manages the state of the SignUpForm and communicates with the AuthenticationRepository in order to create new user accounts.

State

The SignUpState reuses the same Email and Password form input models because the validation logic is the same.

lib/sign_up/cubit/sign_up_state.dart
part of 'sign_up_cubit.dart';
final class SignUpState extends Equatable {
const SignUpState({
this.email = const Email.pure(),
this.password = const Password.pure(),
this.confirmedPassword = const ConfirmedPassword.pure(),
this.status = FormzSubmissionStatus.initial,
this.isValid = false,
this.errorMessage,
});
final Email email;
final Password password;
final ConfirmedPassword confirmedPassword;
final FormzSubmissionStatus status;
final bool isValid;
final String? errorMessage;
@override
List<Object?> get props => [
email,
password,
confirmedPassword,
status,
isValid,
errorMessage,
];
SignUpState copyWith({
Email? email,
Password? password,
ConfirmedPassword? confirmedPassword,
FormzSubmissionStatus? status,
bool? isValid,
String? errorMessage,
}) {
return SignUpState(
email: email ?? this.email,
password: password ?? this.password,
confirmedPassword: confirmedPassword ?? this.confirmedPassword,
status: status ?? this.status,
isValid: isValid ?? this.isValid,
errorMessage: errorMessage ?? this.errorMessage,
);
}
}

Cubit

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.

lib/sign_up/cubit/sign_up_cubit.dart
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);
emit(
state.copyWith(
email: email,
isValid: Formz.validate([
email,
state.password,
state.confirmedPassword,
]),
),
);
}
void passwordChanged(String value) {
final password = Password.dirty(value);
final confirmedPassword = ConfirmedPassword.dirty(
password: password.value,
value: state.confirmedPassword.value,
);
emit(
state.copyWith(
password: password,
confirmedPassword: confirmedPassword,
isValid: Formz.validate([
state.email,
password,
confirmedPassword,
]),
),
);
}
void confirmedPasswordChanged(String value) {
final confirmedPassword = ConfirmedPassword.dirty(
password: state.password.value,
value: value,
);
emit(
state.copyWith(
confirmedPassword: confirmedPassword,
isValid: Formz.validate([
state.email,
state.password,
confirmedPassword,
]),
),
);
}
Future<void> signUpFormSubmitted() async {
if (!state.isValid) return;
emit(state.copyWith(status: FormzSubmissionStatus.inProgress));
try {
await _authenticationRepository.signUp(
email: state.email.value,
password: state.password.value,
);
emit(state.copyWith(status: FormzSubmissionStatus.success));
} on SignUpWithEmailAndPasswordFailure catch (e) {
emit(
state.copyWith(
errorMessage: e.message,
status: FormzSubmissionStatus.failure,
),
);
} catch (_) {
emit(state.copyWith(status: FormzSubmissionStatus.failure));
}
}
}

Sign Up Form

The SignUpForm is responsible for rendering the form in response to the SignUpState and invokes methods on the SignUpCubit in response to user interactions.

lib/sign_up/view/sign_up_form.dart
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});
@override
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)
..hideCurrentSnackBar()
..showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'Sign Up Failure')),
);
}
},
child: Align(
alignment: const Alignment(0, -1 / 3),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_EmailInput(),
const SizedBox(height: 8),
_PasswordInput(),
const SizedBox(height: 8),
_ConfirmPasswordInput(),
const SizedBox(height: 8),
_SignUpButton(),
],
),
),
);
}
}
class _EmailInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
final displayError = context.select(
(SignUpCubit cubit) => cubit.state.email.displayError,
);
return TextField(
key: const Key('signUpForm_emailInput_textField'),
onChanged: (email) => context.read<SignUpCubit>().emailChanged(email),
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'email',
helperText: '',
errorText: displayError != null ? 'invalid email' : null,
),
);
}
}
class _PasswordInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
final displayError = context.select(
(SignUpCubit cubit) => cubit.state.password.displayError,
);
return TextField(
key: const Key('signUpForm_passwordInput_textField'),
onChanged: (password) =>
context.read<SignUpCubit>().passwordChanged(password),
obscureText: true,
decoration: InputDecoration(
labelText: 'password',
helperText: '',
errorText: displayError != null ? 'invalid password' : null,
),
);
}
}
class _ConfirmPasswordInput extends StatelessWidget {
@override
Widget build(BuildContext context) {
final displayError = context.select(
(SignUpCubit cubit) => cubit.state.confirmedPassword.displayError,
);
return TextField(
key: const Key('signUpForm_confirmedPasswordInput_textField'),
onChanged: (confirmPassword) =>
context.read<SignUpCubit>().confirmedPasswordChanged(confirmPassword),
obscureText: true,
decoration: InputDecoration(
labelText: 'confirm password',
helperText: '',
errorText: displayError != null ? 'passwords do not match' : null,
),
);
}
}
class _SignUpButton extends StatelessWidget {
@override
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,
);
return ElevatedButton(
key: const Key('signUpForm_continue_raisedButton'),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
backgroundColor: Colors.orangeAccent,
),
onPressed: isValid
? () => context.read<SignUpCubit>().signUpFormSubmitted()
: null,
child: const Text('SIGN UP'),
);
}
}

Home Page

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.

lib/home/view/home_page.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/home/home.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
static Page<void> page() => const MaterialPage<void>(child: HomePage());
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
final user = context.select((AppBloc bloc) => bloc.state.user);
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: <Widget>[
IconButton(
key: const Key('homePage_logout_iconButton'),
icon: const Icon(Icons.exit_to_app),
onPressed: () {
context.read<AppBloc>().add(const AppLogoutPressed());
},
),
],
),
body: Align(
alignment: const Alignment(0, -1 / 3),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
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.