Setup

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

flutter create flutter_firebase_login

We can then replace the contents of pubspec.yaml with

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 flutter logo asset (which we'll use later).

Then install all of the dependencies:

flutter packages get

The last thing we need to do is follow the firebase_auth usage instructions in order to hook up our application to firebase and enable google_signin.

User Repository

Just like in the flutter login tutorial, we’re going to need to create our UserRepository which will be responsible for abstracting the underlying implementation for how we authenticate and retrieve user information.

Let’s create user_repository.dart and get started.

We can start by defining our UserRepository class and implementing the constructor. You can immediately see that the UserRepository will have a dependency on both FirebaseAuth and GoogleSignIn .

Note: If FirebaseAuth and/or GoogleSignIn are not injected into the UserRepository , then we instantiate them internally. This allows us to be able to inject mock instances so that we can easily test the UserRepository .

The first method we’re going to implement we will call signInWithGoogle and it will authenticate the user using the GoogleSignIn package.

Next, we’ll implement a signInWithCredentials method which will allow users to sign in with their own credentials using FirebaseAuth .

Up next, we need to implement a signUp method which allows users to create an account if they choose not to use Google Sign In.

We need to implement a signOut method so that we can give users the option to logout and clear their profile information from the device.

Lastly, we will need two additional methods: isSignedIn and getUser to allow us to check if a user is already authenticated and to retrieve their information.

Note: getUser is only returning the current user's email address for the sake of simplicity but we can define our own User model and populate it with a lot more information about the user in more complex applications.

Our finished user_repository.dart should look like this:

Next up, we’re going to build our AuthenticationBloc which will be responsible for handling the AuthenticationState of the application in response to AuthenticationEvents .

Authentication States

We need to determine how we’re going to manage the state of our application and create the necessary blocs (business logic components).

At a high level, we’re going to need to manage the user’s Authentication State. A user’s authentication state can be one of the following:

uninitialized — waiting to see if the user is authenticated or not on app start.

authenticated — successfully authenticated

unauthenticated — not authenticated

Each of these states will have an implication on what the user sees.

For example:

if the authentication state was uninitialized, the user might be seeing a splash screen

if the authentication state was authenticated, the user might see a home screen.

if the authentication state was unauthenticated, the user might see a login form.

It’s critical to identify what the different states are going to be before diving into the implementation.

Now that we have our authentication states identified, we can implement our AuthenticationState class.

Create a folder/directory called authentication_bloc and we can create our authentication bloc files.

├── authentication_bloc

│ ├── authentication_bloc.dart

│ ├── authentication_event.dart

│ ├── authentication_state.dart

│ └── bloc.dart

Tip: You can use the IntelliJ or VSCode extensions to autogenerate the files for you.

Note: The equatable package is used in order to be able to compare two instances of AuthenticationState . By default, == returns true only if the two objects are the same instance.

Note: toString is overridden to make it easier to read an AuthenticationState when printing it to the console or in Transitions .

Since we’re using Equatable to allow us to compare different instances of AuthenticationState we need to pass any properties to the superclass. Without super([displayName]) , we will not be able to properly compare different instances of Authenticated .

Authentication Events

Now that we have our AuthenticationState defined we need to define the AuthenticationEvents which our AuthenticationBloc will be reacting to.

We will need:

an AppStarted event to notify the bloc that it needs to check if the user is currently authenticated or not.

event to notify the bloc that it needs to check if the user is currently authenticated or not. a LoggedIn event to notify the bloc that the user has successfully logged in.

event to notify the bloc that the user has successfully logged in. a LoggedOut event to notify the bloc that the user has successfully logged out.

Authentication Barrel File

Before we get to work on the AuthenticationBloc implementation, we will export all authentication bloc files from our authentication_bloc/bloc.dart barrel file. This will allow us import the AuthenticationBloc , AuthenticationEvents , and AuthenticationState with a single import later on.

Authentication Bloc

Now that we have our AuthenticationState and AuthenticationEvents defined, we can get to work on implementing the AuthenticationBloc which is going to manage checking and updating a user's AuthenticationState in response to AuthenticationEvents .

We’ll start off by creating our AuthenticationBloc class.

Note: Just from reading the class definition, we already know this bloc is going to be converting AuthenticationEvents into AuthenticationStates .

Note: Our AuthenticationBloc has a dependency on the UserRepository .

We can start by overriding initialState to the AuthenticationUninitialized() state.

Now all that’s left is to implement mapEventToState .

We created separate private helper functions to convert each AuthenticationEvent into the proper AuthenticationState in order to keep mapEventToState clean and easy to read.

Note: We are using yield* (yield-each) in mapEventToState to separate the event handlers into their own functions. yield* inserts all the elements of the subsequence into the sequence currently being constructed, as if we had an individual yield for each element.

Our complete authentication_bloc.dart should now look like this:

Now that we have our AuthenticationBloc fully implemented, let’s get to work on the presentational layer.

App

We’ll start by removing everything from out main.dart and implementing our main function.

Next we need to implement our App widget.

App will be a StatefulWidget because it will need to manage our AuthenticationBloc .

We create an instance of UserRepository in our _AppState class and then inject it into our AuthenticationBloc in initState . Since we created the AuthenticationBloc in _AppState we need to clean up after ourselves and dispose of it in _AppState .

We are using BlocProvider in order to make our _authenticationBloc instance available to the entire Widget sub-tree. We are also using BlocBuilder in order to render UI based on the _authenticationBloc state.

So far we don’t have any widgets to render but we’ll come back to this once we make our SplashScreen , LoginScreen , and HomeScreen .

Bloc Delegate

Before we get too far along, it’s always handy to implement our own BlocDelegate which allows us to override onTransition and onError and will help us see all bloc state changes (transitions) and errors in one place!

Create simple_bloc_delegate.dart and let's quickly implement our own delegate.

Now we can hook up our BlocDelegate in our main.dart .

Splash Screen

Next, we’ll need to make a SplashScreen widget which will be rendered while our AuthenticationBloc determines whether or not a user is logged in.

Let’s create splash_screen.dart and implement it!

As you can tell, this widget is super minimal and you would probably want to add some sort of image or animation in order to make it look nicer. For the sake of simplicity, we’re just going to leave it as is.

Now, let’s hook it up to our main.dart .

Now whenever our AuthenticationBloc has a currentState of Uninitialized we will render our SplashScreen widget!

Home Screen

Next, we will need to create our HomeScreen so that we can navigate users there once they have successfully logged in. In this case, our HomeScreen will allow the user to logout and also will display their current name (email).

Let’s create home_screen.dart and get started.

HomeScreen is a StatelessWidget that requires a name to be injected so that it can render the welcome message. It also uses BlocProvider in order to access the AuthenticationBloc via BuildCOntext so that when a user pressed the logout button, we can dispatch the LoggedOut event.

Now let’s update our App to render the HomeScreen if the AuthenticationState is Authentication .

Login States

It’s finally time to start working on the login flow. We’ll start by identifying the different LoginStates that we'll have.

Create a login directory and create the standard bloc directory and files.

├── lib

│ ├── login

│ │ ├── bloc

│ │ │ ├── bloc.dart

│ │ │ ├── login_bloc.dart

│ │ │ ├── login_event.dart

│ │ │ └── login_state.dart

Our login/bloc/login_state.dart should look like:

The states we’re representing are:

empty is the initial state of the LoginForm.

loading is the state of the LoginForm when we are validating credentials

failure is the state of the LoginForm when a login attempt has failed.

success is the state of the LoginForm when a login attempt has succeeded.

We have also defined a copyWith and an update function for convenience (which we'll put to use shortly).

Now that we have the LoginState defined let’s take a look at the LoginEvent class.

Login Events

Open up login/bloc/login_event.dart and let's define and implement our events.

The events we defined are:

EmailChanged - notifies the bloc that the user has changed the email

PasswordChanged - notifies the bloc that the user has changed the password

Submitted - notifies the bloc that the user has submitted the form

LoginWithGooglePressed - notifies the bloc that the user has pressed the Google Sign In button

LoginWithCredentialsPressed - notifies the bloc that the user has pressed the regular sign in button.

Login Barrel File

Before we implement the LoginBloc , let's make sure our barrel file is done so that we can easily import all Login Bloc related files with a single import.

Login Bloc

It’s time to implement our LoginBloc . As always, we need to extend Bloc and define our initialState as well as mapEventToState .

Note: We’re overriding transform in order to debounce the EmailChanged and PasswordChanged events so that we give the user some time to stop typing before validating the input.

We are using a Validators class to validate the email and password which we're going to implement next.

Validators

Let’s create validators.dart and implement our email and password validation checks.

There’s nothing special going on here. It’s just some plain old Dart code which uses regular expressions to validate the email and password. At this point, we should have a fully functional LoginBloc which we can hook up to the UI.

Login Screen

Now that we’re finished the LoginBloc it's time to create our LoginScreen widget which will be responsible for creating and disposing the LoginBloc as well as providing the Scaffold for our LoginForm widget.

Create login/login_screen.dart and let's implement it.

Again, we are extending StatefulWidget so that we can initialize the LoginBloc in initState and dispose it in the dispose override. You'll also notice that we are using BlocProvider again in order to make the _loginBloc instance available to all widgets within the sub-tree.

At this point, we need to implement the LoginForm widget which will be responsible for displaying the form and submission buttons in order for a user to authenticate his/her self.

Login Form

Create login/login_form.dart and let's build out our form.

Our LoginForm widget is a StatefulWidget because it needs to maintain it's own TextEditingControllers for the email and password input.

We use a BlocListener widget in order to execute one-time actions in response to state changes. In this case, we are showing different SnackBar widgets in response to a pending/failure state. In addition, if the submission is successful, we use the listener method to notify the AuthenticationBloc that the user has successfully logged in.

Tip: Check out the BlocListener Recipe for more details.

We use a BlocBuilder widget in order to rebuild the UI in response to different LoginStates .

Whenever the email or password changes, we dispatch an event to the LoginBloc in order for it to validate the current form state and return the new form state.

Note: We’re using Image.asset to load the flutter logo from our assets directory.

At this point, you’ll notice we haven’t implemented LoginButton , GoogleLoginButton , or CreateAccountButton so we'll do those next.

Login Button

Create login/login_button.dart and let's quickly implement our LoginButton widget.

There’s nothing special going on here; just a StatelessWidget which has some styling and an onPressed callback so that we can have a custom VoidCallback whenever the button is pressed.

Google Login Button

Create login/google_login_button.dart and let's get to work on our Google Sign In.

Again, there’s not too much going on here. We have another StatelessWidget ; however, this time we are not exposing an onPressed callback. Instead, we're handling the onPressed internally and dispatching the LoginWithGooglePressed event to our LoginBloc which will handle the Google Sign In process.

Note: We’re using font_awesome_flutter for the cool google icon.

Create Account Button

The last of the three buttons is the CreateAccountButton . Let's create login/create_account_button.dart and get to work.

In this case, again we have a StatelessWidget and again we're handling the onPressed callback internally. This time, however, we're pushing a new route in response to the button press to the RegisterScreen . Let's build that next!

Register States

Just like with login, we’re going to need to define our RegisterStates before proceeding.

Create a register directory and create the standard bloc directory and files.

├── lib

│ ├── register

│ │ ├── bloc

│ │ │ ├── bloc.dart

│ │ │ ├── register_bloc.dart

│ │ │ ├── register_event.dart

│ │ │ └── register_state.dart

Our register/bloc/register_state.dart should look like:

Note: The RegisterState is very similar to the LoginState and we could have created a single state and shared it between the two; however, it's very likely that the Login and Register features will diverge and in most cases it's best to keep them decoupled.

Next, we’ll move on to the RegisterEvent class.

Register Events

Open up register/bloc/register_event.dart and let's implement our events.

Register Barrel File

Again, just like with login, we need to create a barrel file to export our register bloc related files.

Open up bloc.dart in our register/bloc directory and export the three files.

Register Bloc

Now, let’s open register/bloc/register_bloc.dart and implement the RegisterBloc .

Just as before, we need to extend Bloc , implement initialState , and mapEventToState . Optionally, we are overriding transform again so that we can give users some time to finish typing before we validate the form.

Now that the RegisterBloc is fully functional, we just need to build out the presentation layer.

Register Screen

Similar to the LoginScreen , our RegisterScreen will be a StatefulWidget responsible for initializing and disposing the RegisterBloc . It will also provide the Scaffold for the RegisterForm .

Create register/register_screen.dart and let's implement it.

Register Form

Next, let’s create the RegisterForm which will provide the form fields for a user to create his/her account.

Create register/register_form.dart and let's build it.

Again, we need to manage TextEditingControllers for the text input so our RegisterForm needs to be a StatefulWidget . In addition, we are using BlocListener again in order to execute one-time actions in response to state changes such as showing SnackBar when the registration is pending or fails. We are also dispatching the LoggedIn event to the AuthenticationBloc if the registration was a success so that we can immediately log the user in.

Note: We’re using BlocBuilder in order to make our UI respond to changes in the RegisterBloc state.

Let’s build our RegisterButton widget next.

Register Button

Create register/register_button.dart and let's get started.

Very similar to how we setup the LoginButton , the RegisterButton has some custom styling and exposes a VoidCallback so that we can handle whenever a user presses the button in the parent widget.

All that’s left to do is update our App widget in main.dart to show the LoginScreen if the AuthenticationState is Unauthenticated .

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.

If you enjoyed this exercise as much as I did you can support me by ⭐️the repository, or 👏 for this story.