Flutter Login Tutorial with “flutter_bloc”
⚠️ This article may be out of date. Please view the updated tutorial at bloclibrary.dev.
If you’re working on a Flutter app, odds are you’re going to need to implement login.
In this article we’re going to walk through one possible way to implement a login flow which will end up looking something like this.
The first thing we’re going to need to do is add flutter_bloc as a dependency to our new flutter project. Our pubspec.yaml
should look something like:
Next, we’re going to need to determine how we’re going to manage the state of our application and create the necessary blocs (business logic components).
In this context, a bloc simply takes a stream of events as input and transforms them into a stream of states as output.
Events are the input to a Bloc. They are commonly UI events such as button presses. Events
are dispatched
and then converted to States
.
States are the output of a Bloc. Presentation components can listen to the stream of states and redraw portions of themselves based on the given state (see BlocBuilder
for more details).
Check out the bloc package if you haven’t already.
We’re going to need to create a UserRepository
which helps us manage a user's data.
Our user repository is just mocking all of the different implementations for the sake of simplicity but in a real application you might inject a HttpClient as well as something like Flutter Secure Storage in order to request tokens and read/write them to keystore/keychain.
At a high level, we’re going to need to manage the user’s AuthenticationState
which we can represent like:
uninitialized
— waiting to see if the user is authenticated or not on app start.loading
— waiting to persist/delete a tokenauthenticated
— successfully authenticatedunauthenticated
— 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 loading, the user might be seeing a progress indicator.
- 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 AuthenticationState
defined we need to define the AuthenticationEvents
which our bloc will be reacting to.
AppStarted
will be dispatched when the Flutter application first loads. It will notify bloc that it needs to determine whether or not there is an existing user.
LoggedIn
will be dispatched on a successful login. It will notify the bloc that the user has successfully logged in.
LoggedOut
will be dispatched on a successful logout. It will notify the bloc that the user has successfully logged out.
Now that we have defined the events and states related to authentication, we can create our AuthenticationBloc
.
The initialState
of the bloc is set to AuthenticationUninitialized
since the first thing our app will need to do is determine whether or not a user is logged in.
mapEventToState
is where our bloc converts the incoming events into states the are consumed by the presentational layer.
Now that we have our AuthenticationBloc
fully implemented, let’s get to work on the presentational layer.
The first thing we’ll need is a SplashPage
widget which will serve as our Splash Screen while our bloc determines whether or not a user is logged in.
Next, we will need to create our HomePage
so that we can navigate users there once they have successfully logged in.
You’re probably wondering what BlocProvider.of<AuthenticationBloc>(context)
is doing. We’ll come back to that in a bit but for now all we need to know is that our HomePage
requires an AuthenticationBloc
to be injected.
Next up, we need to create a LoginPage
and LoginForm
. Because the LoginForm will have to handle user input (Login Button Pressed) and will need to have some business logic (getting a token for a given username/password), we will need to create a LoginBloc
.
Just like we did for the AuthenticationBloc
, we will need to define the LoginState
, and LoginEvents
. Let’s start with LoginState
.
LoginInitial
is the initial state of the LoginForm.
LoginLoading
is the state of the LoginForm when we are validating credentials
LoginFailure
is the state of the LoginForm when a login attempt has failed.
Now that we have the LoginState
defined let’s take a look at the LoginEvent
class.
LoginButtonPressed
will be dispatched when a user pressed the login button. It will notify the bloc that it needs to request a token for the given credentials.
We can now implement our LoginBloc
.
Our LoginBloc
defines it’s initial state as LoginInitial
.
As always, our LoginBloc
has to implement mapEventToState
.
LoginBloc
has a dependency on UserRepository
in order to authenticate a user given a username and password. In addition, LoginBloc
has a dependency on AuthenticationBloc
in order to update the AuthenticationState
when a user has entered valid credentials.
Now that we have our LoginBloc
we can start working on LoginPage
and LoginForm
.
The LoginPage
widget will serve as our container widget and will provide the necessary dependencies to the LoginForm
widget (LoginBloc
and AuthenticationBloc
).
Note that the LoginPage
widget creates the LoginBloc
as part of its state and handles disposing the bloc.
Note that we are using the injected UserRepository
in order to create our LoginBloc
.
You’re probably still wondering what BlocProvider.of<AuthenticationBloc>(context)
is doing. Don’t worry, we’ll come back to that momentarily but for now all we need to know is that our LoginForm
requires an AuthenticationBloc
and a LoginBloc
to be injected.
Next up, let’s go ahead and create our LoginForm
.
Our LoginForm
uses the BlocBuilder
widget so that it can rebuild whenever there is a new LoginState
.
BlocBuilder is a Flutter widget which requires a Bloc
and a builder
function. BlocBuilder
handles building the widget in response to new states. BlocBuilder
is very similar to StreamBuilder
but has a more simple API to reduce the amount of boilerplate code needed.
There’s not much else going on in the LoginForm
widget so let’s move on to creating our loading indicator.
Now it’s finally time to put it all together and create our main App
widget in main.dart
.
Our app has an injected AuthenticationBloc
which is makes available to the entire widget tree by using theBlocProvider
widget.
BlocProvider is a Flutter widget which provides a bloc to its children via BlocProvider.of(context)
. It is used as a DI widget so that a single instance of a bloc can be provided to multiple widgets within a subtree.
Now BlocProvider.of<AuthenticationBloc>(context)
in our HomePage
and LoginPage
widget should make more sense. Since we wrapped our MaterialApp within a BlocProvider<AuthenticationBloc>
we can access the instance of our AuthenticationBloc
by using the BlocProvider.of<AuthenticationBloc>(BuildContext context)
static method.
Again, we are using BlocBuilder
in order to react to changes in AuthenticationState
so that we can show the user either the SplashPage
, LoginPage
, or HomePage
based on the current AuthenticationState
.
One added bonus of using bloc is that we can have access to all Transitions
in one place. It's fairly common in large applications to have many blocs managing different parts of the application's state.
If we want to be able to do something in response to all Transitions
we can simply create our own BlocDelegate
which we have named SimpleBlocDelegate
.
In order to use our SimpleBlocDelegate
, we just need to tweak our main
function to set the BlocSupervisor's
delegate to our instance of SimpleBlocDelegate
. See the official bloc library documentation for more details.
At this point we have a pretty solid login implementation and we have decoupled our presentation layer from the business logic layer by using bloc and flutter_bloc. The full source is available here.
If you enjoyed this exercise as much as I did you can support me by ⭐️the repository, or 👏 for this story.