Flutter signup/login application with Django backend #3
Tags: flutter, bloc, dart, django
Published: 2020-04-20 11:29:26

# What this post is about
In the previous two posts, I made an API in Django with the help of DRF (please read them: <a href="https://ohuru.tech/blog/2020/4/15/flutter-signuplogin-application-with-django-backend-1/">post #1</a>, <a href="https://ohuru.tech/blog/2020/4/18/flutter-signuplogin-application-with-django-backend-2/">post #2</a> before this post in case you want to develop the API as well). This post will be about the flutter application that will function with that API. We are going to create an application to allow a user to log in and log out.

You can also view the source code at <a href="https://github.com/amartya-dev/flutter-bloc/tree/master/bloc_login">this repo</a>.

The application demo:
{% youtube iFO7trHwvv0 %}

# Prerequisites
To follow along with the post, you need to have flutter installed (refer: https://flutter.dev/docs/get-started/install) along with visual studio code and the extension for flutter (refer: https://flutter.dev/docs/development/tools/vs-code) and bloc (refer: https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc).

# Let's Start
- Begin with creating a project (it would be better to use the vscode extension for the same) called bloc_login (Ctrl + shift + p, type flutter and you will get an option to create a new project).
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7qj5phyxxr5x6gjiqe85.png)

- Next, we will create certain folders in order to separate different components of our code. Create the following directories in bloc_login/
    - api_connection (will contain the code to communicate with our API)
    - common (Will contain utility widgets like loading indicator)
    - dao (Will contain a helper file in order to communicate with our sqlite database)
    - model (Will contain schema of our database)
    - repository (Will act as a bridge between our API, blocs, and database)
    - splash (Will contain splash screen code)
    - login (Will contain login form and other details pertaining to it)
    - database (Will contain files in order to initialize db and create the table to start  inserting data)
    - home (Will contain our main UI)

- You can refer to <a href="https://bloclibrary.dev/#/flutterbloccoreconcepts">this</a> page in order to get a basic understanding of how flutter blocs work. To understand what blocs actually are you can give <a href="https://bloclibrary.dev/#/coreconcepts">this</a> page a read. In short, they are a way to map our incoming events to states which flutter will consume in order to rebuild UI in accordance.

- Add dependencies. We are going to use Equatable, flutter_bloc, sqflite and http packages, thus modify your pubspec.yaml file and include the following dependencies: 
```dart
dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^3.2.0
  meta: ^1.1.6
  equatable: ^1.1.0
  http: ^0.12.0+4
  sqflite: ^1.3.0
  path_provider: ^1.6.5
  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
```
Note: I have just shown the dependencies part where you need to add the files for dependencies.
Just saving the file in VScode will automatically trigger the command to get these packages but, for reference, run the following command from your <code>bloc_login/</code> directory:
```bash
flutter pub get
```

- Let us start by making our database helper thus, create a file <code>user_database.dart</code> inside your <code>bloc_directory/database</code> directory and add the following code to it: 
```dart
import 'dart:async';
import 'dart:io';

import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';

final userTable = 'userTable';

class DatabaseProvider {
  static final DatabaseProvider dbProvider = DatabaseProvider();

  Database _database;

  Future <Database> get database async {
    if (_database != null){
      return _database;
    }
    _database = await createDatabase();
    return _database;
  }

  createDatabase() async {
    Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, "User.db");

    var database = await openDatabase(
      path,
      version: 1,
      onCreate: initDB,
      onUpgrade: onUpgrade,
    );
    return database;
  }

  void onUpgrade(
    Database database,
    int oldVersion,
    int newVersion,
  ){
    if (newVersion > oldVersion){}
  }

  void initDB(Database database, int version) async {
    await database.execute(
      "CREATE TABLE $userTable ("
      "id INTEGER PRIMARY KEY, "
      "username TEXT, "
      "token TEXT "
      ")"
    );
  }
}
```

Here we return the opened database as the <code>database</code> variable in our DatabaseProvider class. Thus, <code>dbProvider</code> will initialize the <code>DatabaseProvider</code> class and will have a get database function that will return the opened database.

We can use the <code>onUpgrade</code> function in case we need to migrate some changes between different versions. Also our final <code>userTable</code> variable is used to store the table name for our database.

- Let us next create our model for the user that corresponds to the table we just created. Thus, create a file <code>bloc_login/model/user_model.dart</code> and add the following code to it:
```dart
class User {
  int id;
  String username;
  String token;

  User(
      {this.id,
      this.username,
      this.token});

  factory User.fromDatabaseJson(Map<String, dynamic> data) => User(
      id: data['id'],
      username: data['username'],
      token: data['token'],
  );

  Map<String, dynamic> toDatabaseJson() => {
        "id": this.id,
        "username": this.username,
        "token": this.token
      };
}
```
Here we also defined the factory function fromDatabaseJson which will return the user as a JSON object and the toDatabaseJson which will be responsible for converting the incoming JSON object to a database object which can be stored to our database.

- Let us add API functionality thus create the file <code>bloc_login/api_connection/api_connection.dart</code> and add the following code to it:
```dart
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:bloc_login/model/api_model.dart';

final _base = "https://home-hub-app.herokuapp.com";
final _tokenEndpoint = "/api-token-auth/";
final _tokenURL = _base + _tokenEndpoint;

Future<Token> getToken(UserLogin userLogin) async {
  print(_tokenURL);
  final http.Response response = await http.post(
    _tokenURL,
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(userLogin.toDatabaseJson()),
  );
  if (response.statusCode == 200) {
    return Token.fromJson(json.decode(response.body));
  } else {
    print(json.decode(response.body).toString());
    throw Exception(json.decode(response.body));
  }
}
```
I have used the above URL because I named my app home_hub for heroku, you need to replace that with the URL you got from the <a href="https://dev.to/amartyadev/flutter-signup-login-application-with-django-backend-2-4kn5">previous post</a>. The rest can remain the same.

We just have a helper function here in order to get token for any particular user and throw an error in case the details are not right.

- Let us add a model in order to deal with requests to API and subsequent responses. Create a file <code>bloc_login/model/api_model.dart</code> and add the following code to it:
```dart
class UserLogin {
  String username;
  String password;

  UserLogin({this.username, this.password});

  Map <String, dynamic> toDatabaseJson() => {
    "username": this.username,
    "password": this.password
  };
}

class Token{
  String token;

  Token({this.token});

  factory Token.fromJson(Map<String, dynamic> json) {
    return Token(
      token: json['token']
    );
  }
}
```
We are going to use this model in order to just get the username and password from the user and then send them to the server. The Token class gets the result and gives us the string from the JSON object we receive from the server.

- Let us create a dao object to provide some basic operations for User model and the database, create a file <code>bloc_login/dao/user_dao.dart</code> and add the following code to it:
```dart
import 'package:bloc_login/database/user_database.dart';
import 'package:bloc_login/model/user_model.dart';

class UserDao {
  final dbProvider = DatabaseProvider.dbProvider;

  Future<int> createUser(User user) async {
    final db = await dbProvider.database;

    var result = db.insert(userTable, user.toDatabaseJson());
    return result;
  }

  Future<int> deleteUser(int id) async {
    final db = await dbProvider.database;
    var result = await db
        .delete(userTable, where: "id = ?", whereArgs: [id]);
    return result;
  }

  Future<bool> checkUser(int id) async {
    final db = await dbProvider.database;
    try {
      List<Map> users = await db
          .query(userTable, where: 'id = ?', whereArgs: [id]);
      if (users.length > 0) {
        return true;
      } else {
        return false;
      }
    } catch (error) {
      return false;
    }
  }
}
```
This basically provides us with methods to create a user / delete a user and search if a user exists or not. Since our app will provide login facilities to only one user at max, we are forcing the user to be created with id 0. Thus, we can always check for the existence of a user by checking the first entry in our database. The best part is that we are just storing the username and token and not the password entered. The password is just used to send a request to the server and we never store than anywhere else. There is an alternative here, you can always use <a href="https://pub.dev/packages/flutter_secure_storage">flutter_secure_storage</a> in order to store the token and use it but I wanted to use SQL as I want to further extend the application to include other functionalities.

- Let us come to the logical implementation of our apps i.e. the blocs. I recommend using the vscode extension to create a new bloc (refer: <a href="https://bloclibrary.dev/#/blocvscodeextension">here</a>) although, you can always manually create the files. Create new bloc in <code>bloc_login.</code> directory and name it authentication_bloc.
The extension will automatically create the <code>bloc_login/bloc</code> folder and create three files in it corresponding to the states, events and the bloc for authentication_bloc. Modify your <code>bloc_login/bloc/authentication_state.dart</code> and add the following code:
```dart
part of 'authentication_bloc.dart';

abstract class AuthenticationState extends Equatable {
  @override
  List<Object> get props => [];
}

class AuthenticationUnintialized extends AuthenticationState {}

class AuthenticationAuthenticated extends AuthenticationState {}

class AuthenticationUnauthenticated extends AuthenticationState {}

class AuthenticationLoading extends AuthenticationState {}
```

Here we define four authentication states which are Uninitialized which can correspond to the state when the app is waiting to check if a user exists in our database or not, the loading state which can be a state when we are waiting for the app to store a token or delete a token, authenticated state corresponding to the successful login of a user and unauthenticated which corresponds to the user not being authenticated yet / logged out.

- Let us define the events for our authentication bloc modify your <code>bloc_login/bloc/authentication_event.dart</code> and add the following code:
```dart
part of 'authentication_bloc.dart';

abstract class AuthenticationEvent extends Equatable {
  const AuthenticationEvent();

  @override
  List<Object> get props => [];
}

class AppStarted extends AuthenticationEvent {}

class LoggedIn extends AuthenticationEvent {
  final User user;

  const LoggedIn({@required this.user});

  @override
  List<Object> get props => [user];

  @override
  String toString() => 'LoggedIn { user: $user.username.toString() }';
}

class LoggedOut extends AuthenticationEvent {}
```

This corresponds to three events that might occur, AppStarted which will notify the block to check if a user exists, LoggedIn which will be the event suggesting that the user has logged in successfully and LoggedOut which will tell that the user has logged out.

- Now we need to implement our bloc that is map our events to states, modify your <code>bloc_login/bloc/authentication_bloc.dart</code> and add the following:

```dart
import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';

import 'package:bloc_login/repository/user_repository.dart';
import 'package:bloc_login/model/user_model.dart';

part 'authentication_event.dart';
part 'authentication_state.dart';

class AuthenticationBloc
    extends Bloc<AuthenticationEvent, AuthenticationState> {
  final UserRepository userRepository;

  AuthenticationBloc({@required this.userRepository})
      : assert(UserRepository != null);

  @override
  AuthenticationState get initialState => AuthenticationUnintialized();

  @override
  Stream<AuthenticationState> mapEventToState(
    AuthenticationEvent event,
  ) async* {
    if (event is AppStarted) {

      final bool hasToken = await userRepository.hasToken();

      if (hasToken) {
        yield AuthenticationAuthenticated();
      } else {
        yield AuthenticationUnauthenticated();
      }
    }

    if (event is LoggedIn) {
      yield AuthenticationLoading();

      await userRepository.persistToken(
        user: event.user
      );
      yield AuthenticationAuthenticated();
    }

    if (event is LoggedOut) {
      yield AuthenticationLoading();

      await userRepository.delteToken(id: 0);

      yield AuthenticationUnauthenticated();
    }
  }
}
```
Here we did the following:
    - Initialized state to be AuthenticationUninitialized
    - Yielded the AuthenticationAuthenticated state which corresponds to the existence of users in our database. This will help in the persistence of the state because the user will exist even after we clear our app from the ram.
    - If the event was LoggedIn, we saved the user to the database by calling the persistToken function we defined.
    - If the event was LoggedOut, we deleted the user from the database.

- Enters UI, let us create our splash screen, create the file <code>bloc_login/splash_page.dart</code> and the following code to it:
```dart
import 'package:flutter/material.dart';

class SplashPage extends StatelessWidget {
  @override
  Widget build (BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('Splash Screen'),
      ),
    );
  }
}
```
We will also export this code in the file <code>bloc_login/splash/splash.dart</code> for easy access:
```dart
export 'splash_page.dart';
```
- Let us also create our home page so that we can navigate users when they are logged in. Thus, create the file <code>bloc_login/home/home_page.dart</code> and add the following code:
```dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc_login/bloc/authentication_bloc.dart';

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home | Home Hub'),
      ),
      body: Container(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Padding(padding: EdgeInsets.only(left: 30.0),
            child: Text(
              'Welcome',
              style: TextStyle(
                fontSize: 24.0,
              ),
            ),),
            Padding(
              padding: EdgeInsets.fromLTRB(34.0, 20.0, 0.0, 0.0),
              child: Container(
                width: MediaQuery.of(context).size.width * 0.85,
                height: MediaQuery.of(context).size.width * 0.16,
                child: RaisedButton(
                  child: Text(
                    'Logout',
                    style: TextStyle(
                      fontSize: 24,
                    ),
                  ),
                  onPressed: () {
                    BlocProvider.of<AuthenticationBloc>(context)
                        .add(LoggedOut());
                  },
                  shape: StadiumBorder(
                    side: BorderSide(
                      color: Colors.black,
                      width: 2,
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```
Here we are displaying Welcome and a button to Log Out a user, notice that we got our bloc provider from the build context (we will add the code for it in <code>main.dart</code>) and notified it that the LoggedOut event has occurred by adding that event on button press.

- We also need to create a login bloc to handle our log in states and events. Thus, make another bloc called login using the extension in the <code>bloc_login/login</code> directory we created and add the following code in the <code>bloc_login/login/bloc/login_states.dart</code>
```dart
part of 'login_bloc.dart';

abstract class LoginState extends Equatable {
  const LoginState();

  @override
  List<Object> get props => [];
}

class LoginInitial extends LoginState {}

class LoginLoading extends LoginState {}

class LoginFaliure extends LoginState {
  final String error;

  const LoginFaliure({@required this.error});

  @override
  List<Object> get props => [error];

  @override
  String toString() => ' LoginFaliure { error: $error }';
}
```
Here the LoginInitial state is the initial state for our login form, LoginLoading is the state when our form is validating the credentials and LoginFaliure indicates that the login attempt was unsuccessful.

- Let us create our login events, modify your <code>bloc_login/login/login_event.dart<code> file and add the following code:
```dart
part of 'login_bloc.dart';

abstract class LoginEvent extends Equatable {
  const LoginEvent();
}

class LoginButtonPressed extends LoginEvent {
  final String username;
  final String password;

  const LoginButtonPressed({
    @required this.username,
    @required this.password
  });

  @override
  List<Object> get props => [username, password];

  @override
  String toString() => 'LoginButtonPressed { username: $username, password: $password }';
}
```

Let us also add the login to map the events to state in <code>bloc_login/login/login_bloc.dart</code>. Add the following code in this file:
```dart
import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:bloc_login/bloc/authentication_bloc.dart';
import 'package:bloc_login/repository/user_repository.dart';
import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';

part 'login_event.dart';
part 'login_state.dart';

class LoginBloc extends Bloc<LoginEvent, LoginState> {
  final UserRepository userRepository;
  final AuthenticationBloc authenticationBloc;

  LoginBloc({
    @required this.userRepository,
    @required this.authenticationBloc,
  })  : assert(userRepository != null),
        assert(authenticationBloc != null);

  @override
  LoginState get initialState => LoginInitial();

  @override
  Stream<LoginState> mapEventToState(
    LoginEvent event,
  ) async* {
    if (event is LoginButtonPressed) {
      yield LoginInitial();

      try {
        final user = await userRepository.authenticate(
          username: event.username,
          password: event.password,
        );

        authenticationBloc.add(LoggedIn(user: user));
        yield LoginInitial();
      } catch (error) {
        yield LoginFaliure(error: error.toString());
      }
    }
  }
}
```
- Let us create our login form. Create a file <code>bloc_login/login/login_form.dart</code> and add the following code:
```dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc_login/login/bloc/login_bloc.dart';

class LoginForm extends StatefulWidget {
  @override
  State<LoginForm> createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    _onLoginButtonPressed() {
      BlocProvider.of<LoginBloc>(context).add(LoginButtonPressed(
        username: _usernameController.text,
        password: _passwordController.text,
      ));
    }

    return BlocListener<LoginBloc, LoginState>(
      listener: (context, state) {
        if (state is LoginFaliure) {
          Scaffold.of(context).showSnackBar(SnackBar(
            content: Text('${state.error}'),
            backgroundColor: Colors.red,
          ));
        }
      },
      child: BlocBuilder<LoginBloc, LoginState>(
        builder: (context, state) {
          return Container(
            child: Form(
              child: Padding(
                padding: EdgeInsets.all(40.0),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  crossAxisAlignment: CrossAxisAlignment.center,
                  children: <Widget>[
                    TextFormField(
                      decoration: InputDecoration(
                          labelText: 'username', icon: Icon(Icons.person)),
                      controller: _usernameController,
                    ),
                    TextFormField(
                      decoration: InputDecoration(
                          labelText: 'password', icon: Icon(Icons.security)),
                      controller: _passwordController,
                      obscureText: true,
                    ),
                    Container(
                      width: MediaQuery.of(context).size.width * 0.85,
                      height: MediaQuery.of(context).size.width * 0.22,
                      child: Padding(
                        padding: EdgeInsets.only(top: 30.0),
                        child: RaisedButton(
                          onPressed: state is! LoginLoading
                              ? _onLoginButtonPressed
                              : null,
                          child: Text(
                            'Login',
                            style: TextStyle(
                              fontSize: 24.0,
                            ),
                          ),
                          shape: StadiumBorder(
                            side: BorderSide(
                              color: Colors.black,
                              width: 2,
                            ),
                          ),
                        ),
                      ),
                    ),
                    Container(
                      child: state is LoginLoading
                          ? CircularProgressIndicator()
                          : null,
                    ),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}
```

- Also, we will create a file <code>bloc_login/login/login_page.dart</code> and add the following code:
```dart
import 'package:flutter/material.dart';

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc_login/repository/user_repository.dart';

import 'package:bloc_login/bloc/authentication_bloc.dart';
import 'package:bloc_login/login/bloc/login_bloc.dart';
import 'package:bloc_login/login/login_form.dart';

class LoginPage extends StatelessWidget {
  final UserRepository userRepository;

  LoginPage({Key key, @required this.userRepository})
      : assert(userRepository != null),
        super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Login | Home Hub'),
      ),
      body: BlocProvider(
        create: (context) {
          return LoginBloc(
            authenticationBloc: BlocProvider.of<AuthenticationBloc>(context),
            userRepository: userRepository,
          );
        },
        child: LoginForm(),
      ),
    );
  }
}
```

We are all set, now all that is needed to be done is to create our <code>bloc_login/main.dart</code> file and also create a loading indicator. Let us first create the loading indicator, create <code>bloc_login/common/loading_indicator.dart</code> and add the following code:
```dart
import 'package:flutter/material.dart';

class LoadingIndicator extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Center(
        child: CircularProgressIndicator(),
      );
}
```
We will also export it from <code>bloc_login/common/common.dart</code> for easy access:
```dart
export './loading_indicator.dart';
```

- Finally, add the following code in <code>bloc_login/main.dart</code>
```dart
import 'package:flutter/material.dart';

import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc_login/repository/user_repository.dart';

import 'package:bloc_login/bloc/authentication_bloc.dart';
import 'package:bloc_login/splash/splash.dart';
import 'package:bloc_login/login/login_page.dart';
import 'package:bloc_login/home/home.dart';
import 'package:bloc_login/common/common.dart';

class SimpleBlocDelegate extends BlocDelegate {
  @override
  void onEvent(Bloc bloc, Object event) {
    super.onEvent(bloc, event);
    print(event);
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    print (transition);
  }

  @override
  void onError(Bloc bloc, Object error, StackTrace stacktrace) {
    super.onError(bloc, error, stacktrace);
  }
}

void main() {
  BlocSupervisor.delegate = SimpleBlocDelegate();
  final userRepository = UserRepository();

  runApp(
    BlocProvider<AuthenticationBloc>(
      create: (context) {
        return AuthenticationBloc(
          userRepository: userRepository
        )..add(AppStarted());
      },
      child: App(userRepository: userRepository),
    )
  );
}

class App extends StatelessWidget {
  final UserRepository userRepository;

  App({Key key, @required this.userRepository}) : super(key: key);

  @override
  Widget build (BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.red,
        brightness: Brightness.dark,
      ),
      home: BlocBuilder<AuthenticationBloc, AuthenticationState>(
        builder: (context, state) {
          if (state is AuthenticationUnintialized) {
            return SplashPage();
          }
          if (state is AuthenticationAuthenticated) {
            return HomePage();
          }
          if (state is AuthenticationUnauthenticated) {
            return LoginPage(userRepository: userRepository,);
          }
          if (state is AuthenticationLoading) {
            return LoadingIndicator();
          }
        },
      ),
    );
  }
}
```

I have added the BlocDelegate and overridden the functions for debugging purposes. I know this is a slightly long post but I thought it will be good to include the whole application here for reference.

This shall be the last post in this series. You can also view the source code at <a href="https://github.com/amartya-dev/flutter-bloc/tree/master/bloc_login">this repo</a>.

Links to previous posts:
<a href="https://ohuru.tech/blog/2020/4/15/flutter-signuplogin-application-with-django-backend-1/">Flutter signup/login application with Django backend #1</a>
<a href="https://ohuru.tech/blog/2020/4/18/flutter-signuplogin-application-with-django-backend-2/">Flutter signup/login application with Django backend #2</a>

Please feel free to drop a message at <a href="https://ohuru.tech/">Ohuru</a> in order to avail various development services offered by us.