How to create a simple login form in Flutter™ using BLoC pattern
By: Dlt Labs
Tags: Mobile App Development, Flutter, Android App Development, Forms, Dltlabs

We begin by creating a new flutter project using Android Studio and name it login_demo.

Replace all auto-generated code from main.dart file with the following single line and import statement:

import ‘package:flutter/material.dart’; void main() => runApp(App());

I always prefer to keep the main file as clean as possible. (Ignore the error for now.)

Next, let’s create a proper package structure for the app as provided below.

Once you are done with the folder structure let’s create one file, app.dart, inside the ‘src folder’ and place the code below within the folder.

class App extends StatelessWidget {

@override

Widget build(BuildContext context) {

return MaterialApp(

title: ‘Login Demo’,

theme: ThemeData(

primarySwatch: Colors.blue,

),

home: createContent(),

);

} createContent() {

return Text(“Login Demo app”);

}

}

So far, we have simply created an app’s root widget. You can run it now and can see a black screen with the text “Login Demo app” in red color.

Before writing more code, let me brief you about what we will try to achieve here and how.

We will create a simple Flutter app which follows the BLoC pattern and consists of only two screens: a login screen, and a home screen. The login screen should allow the user to enter any valid credentials (email and password) and be logged in with mock networking.

Each time a session is opened we’ll redirect the user to the home screen.

When integrating the BLoC pattern, each screen will have one bloc file associated. Let’s create one bloc for the above created App screen.

Select New->Dart File, name the file authorization_bloc.dart, and keep it inside the blocs package. Place the code below inside this authorization_bloc file.

import ‘package:rxdart/rxdart.dart’;

import ‘package:shared_preferences/shared_preferences.dart’; class AuthorizationBloc {

String _tokenString = “”;

final PublishSubject _isSessionValid = PublishSubject<bool>();

Observable<bool> get isSessionValid => _isSessionValid.stream; void dispose() {

_isSessionValid.close();

} void restoreSession() async {

SharedPreferences prefs = await SharedPreferences.getInstance();

_tokenString = prefs.get(“token”);

if (_tokenString != null && _tokenString.length > 0) {

_isSessionValid.sink.add(true);

} else {

_isSessionValid.sink.add(false);

}

} void openSession(String token) async {

SharedPreferences prefs = await SharedPreferences.getInstance();

await prefs.setString(“token”, token);

_tokenString = token;

_isSessionValid.sink.add(true);

} void closeSession() async {

SharedPreferences prefs = await SharedPreferences.getInstance();

prefs.remove(“token”);

_isSessionValid.sink.add(false);

}

} final authBloc = AuthorizationBloc();

This bloc will be used inside the App to maintain the session of the application.

This bloc consists of 3 basic functions, each named after the purpose they serve:

openSession(String Token) — This function is responsible for opening the user session, when the user logging in. closeSession- This is responsible for closing the user session, when the user wants to logout from the application. restoreSession — This is responsible for restoring the user session when the user opens the app from a terminated state.

Now, to configure this bloc to the app.dart file, update the createContent() function with the code below:

createContent() {

return StreamBuilder<bool> (

stream: authBloc.isSessionValid,

builder: (context, AsyncSnapshot<bool> snapshot){

if (snapshot.hasData && snapshot.data) {

return HomeScreen();

}

return LoginScreen();

});

}

And add the line authBloc.restoreSession(); at the top of Widget build(BuildContext context) method. Now the final method will look like this:

@override Widget build(BuildContext context) { authBloc.restoreSession(); return MaterialApp( title: ‘Login Demo’, theme: ThemeData( primarySwatch: Colors.blue, ), home: createContent(), ); }

With these changes, we’ve made some progress: every time the user session is updated, it will update the UI accordingly. Ignore the errors for now.

Now let’s create two classes to mock network operations for user login.

Create a new dart file inside the resources package and name it repository.dart. Place this code in the file:

import ‘auth_provider.dart’; class Repository { final AuthProvider authProvider = AuthProvider(); Future<String> login(String email, String password) => authProvider.login(email: email, password: password);

}

Create another file to manage all authorization network operations. Keep it inside the same package, name it auth_provider.dart, and put in:

import ‘package:flutter/material.dart’; class AuthProvider {

Future<String> login({

@required String email,

@required String password,

}) async {

await Future.delayed(Duration(seconds: 1));

return ‘token-info’;

}

}

At this point, I am simply returning a user session token without any operation, and then delaying the response for 1 second.

Now create the Login bloc file inside package blocs with name login_bloc.dart. Enter the code below inside this file:

class LoginBloc extends Validators {

Repository repository = Repository(); final BehaviorSubject _emailController = BehaviorSubject<String>();

final BehaviorSubject _passwordController = BehaviorSubject<String>();

final PublishSubject _loadingData = PublishSubject<bool>(); Function(String) get changeEmail => _emailController.sink.add;

Function(String) get changePassword => _passwordController.sink.add; Stream<String> get email => _emailController.stream.transform(validateEmail);

Stream<String> get password => _passwordController.stream.transform(validatePassword);

Stream<bool> get submitValid => Observable.combineLatest2(email, password, (email, password) => true);

Observable<bool> get loading => _loadingData.stream; void submit() {

final validEmail = _emailController.value;

final validPassword = _passwordController.value;

_loadingData.sink.add(true); login(validEmail, validPassword);

} login(String email, String password) async {

String token = await repository.login(email, password);

_loadingData.sink.add(false);

authBloc.openSession(token);

} void dispose() {

_emailController.close();

_passwordController.close();

_loadingData.close();

}

}

Here we have created three basic rxdart variables: _emailController, _passwordController, _loadingData.

The first two variables are responsible for managing the email and password fields validations and the last one, _loadingData, is used for updating the UI to show the loading of data.

Creating the login screen…

Now create a login screen class file inside the ‘ui package’ with the name login_screen.dart. Then, enter the code below inside this file:

class LoginScreen extends StatefulWidget { @override

State<StatefulWidget> createState() => LoginScreenState();

} class LoginScreenState extends State<LoginScreen> {

LoginBloc bloc = LoginBloc(); @override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(

title: Text(‘Login’),

),

body: Container(

margin: EdgeInsets.all(20.0),

child: Column(

children: <Widget>[

emailField(bloc),

passwordField(bloc),

Container(margin: EdgeInsets.only(top: 25.0)),

submitButton(bloc),

loadingIndicator(bloc)

],

),

),

);

}

} Widget loadingIndicator(LoginBloc bloc) => StreamBuilder<bool>(

stream: bloc.loading,

builder: (context, snap) {

return Container(

child: (snap.hasData && snap.data)

? CircularProgressIndicator()

: null,

);

},

); Widget emailField(LoginBloc bloc) => StreamBuilder<String>(

stream: bloc.email,

builder: (context, snap) {

return TextField(

keyboardType: TextInputType.emailAddress,

onChanged: bloc.changeEmail,

decoration: InputDecoration(

labelText: ‘Email address’,

hintText: ‘you@example.com’,

errorText: snap.error

),

);

},

); Widget passwordField(LoginBloc bloc) => StreamBuilder<String>(

stream: bloc.password,

builder:(context, snap) {

return TextField(

obscureText: true,

onChanged: bloc.changePassword,

decoration: InputDecoration(

labelText: ‘Password’,

hintText: ‘Password’,

errorText: snap.error

),

);

}

);

Widget submitButton(LoginBloc bloc) => StreamBuilder<bool>(

stream: bloc.submitValid,

builder: (context, snap) {

return RaisedButton(

onPressed: (!snap.hasData) ? null : bloc.submit,

child: Text(‘Login’, style: TextStyle(color: Colors.white),),

color: Colors.blue,

);

},

);

So far, we have used 4 basic widgets:

emailField — This is to take email input, connect it with the changeEmail bloc to validate user input, and update the UI, accordingly. passwordField — Similar to emailField, but with password input and validations. loadingIndicator — To show the loading UI during API call wait times. submitButton — This is for the Button to let user login, using his or her credentials. This button will be enabled once email and password text are validated. For that reason, this is also being controlled by block submitValid.

Now when the user successfully logs in, we will redirect him or her to the home screen. As I have explained earlier, every screen will have two files, a business logic file (BLoC File) and a user interface file (UI File).

Creating the home screen…

So let’s create two new files for home screen and name them home_bloc.dart and home_screen.dart.

home_bloc.dart: class HomeBloc { logoutUser() {

authBloc.closeSession();

} } home_screen.dart: class HomeScreen extends StatefulWidget { @override

State<StatefulWidget> createState() => HomeScreenState();

} class HomeScreenState extends State<HomeScreen> {

HomeBloc bloc = HomeBloc();

@override

Widget build(BuildContext context) {

return Scaffold(

appBar: AppBar(title: Text(“Home Screen”),),

body: Center(

child: RaisedButton(

onPressed: bloc.logoutUser,

child: Text(‘Logout’, style: TextStyle(color: Colors.white),),

color: Colors.blue,

),

),

);

}

}

The home screen will consist of only one button, which lets the user logout from the application.

Let’s run the application and see what happens.

As the user, you should see a simple login screen with email and password fields.

Once you provide all valid inputs and select continue, you should be redirected to the home screen.

I have saved the best for last. If you terminate the application and log in again, you will be redirected to the home screen. If you then select log out, you will be redirected back to the login screen. Hurray!

I hope you have gotten an idea about what the BLoC pattern is and how it can help you organize your code and distinguish Business logic with UI.

So, what are you waiting for? Try it in your next project and improve your code architecture!

Happy Coding :)

DLT Labs is a trademark of DLT Global, Inc.. Android and Flutter are trademarks of Google LLC and their use here does not indicate endorsement or affiliation.

Author — Dharmendra Yadav, DLT Labs™

About the Author: Dharmendra Yadav is a mobile developer with varied experience in Mobile Application Development. He has had exposure to mobile application architecture for both iOS and Android.