Flutter: Login App using REST API and SQFLite

Login App using Flutter

Flutter has hit the mobile app development world like a storm. Being an Android app and web application developer, I wanted to try this new framework to see why there’s so much buzz about it. Previously, I was very much impressed by NativeScript and React. For some strange reason, I disliked JSX and hence never used React on any of my projects.

Flutter is a cross-platform app development framework and it works on Android, iOS (and Fuschia?). The beta version of the framework was released just a few days back and Google claims to use it in production for some of its apps. One more interesting part here is that, Flutter is completely written in Dart. But worry not, Dart is very easy to learn if you’ve worked with Java and Javascript. It combines all the great features of Javascript and Java to offer an easy to use and robust modern language. But I’ll be honest, Kotlin is still my favorite modern language.

All the B.S aside, let’s get into the actual demo of building an app with Flutter. The goal of this post is to show you how to build an app with a Login screen and a simple Home screen. I choose this topic since I couldn’t find any topics explaining how to implement a Login app with SQFLite and a REST backend. Of course you can use Firebase Authentication for this, but the point is to make it easy for you to understand the ceremony involved in setting up a REST and DB client in Flutter.

Our app will have two screens:

  • Login Screen (Includes text fields for username and password, and a button for login)
  • Home Screen (Displays “Welcome Home”)

Firstly, I hope you have already setup Flutter in your favorite IDE. I will be using Android Studio here. If you haven’t set it up yet then please click here.

One more thing to note, I will try to follow MVP architecture here but I might be violating few guidelines so please don’t mind.

App Folder Structure

As you can see in the screenshot above, that’s how our app’s structure will be after end of this tutorial. Do not worry, I will go through each of the files in detail.

Navigation and Routes

Our app has only two screens, but we will use the Routing feature built into Flutter to navigate to login screen or home screen depending on the login state of the user.

routes.dart

import 'package:flutter/material.dart';
import 'package:login_app/screens/home/home_screen.dart';
import 'package:login_app/screens/login/login_screen.dart';

final routes = {
'/login': (BuildContext context) => new LoginScreen(),
'/home': (BuildContext context) => new HomeScreen(),
'/' : (BuildContext context) => new LoginScreen(),
};

Adding routes are pretty simple, first you need to setup the different routes as a Map object. We only have three routes, ‘/’ is the default route.

main.dart

import 'package:flutter/material.dart';
import 'package:login_app/auth.dart';
import 'package:login_app/routes.dart';

void main() => runApp(new LoginApp());

class LoginApp extends StatelessWidget {


// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'My Login App',
theme: new ThemeData(
primarySwatch: Colors.red,
),
routes: routes,
);
}


}

main.dart will have the entry point for our app. Nothing new happening here, we just setup MaterialApp widget as root and specify to use the routes that we just defined.

utils/network_util.dart

import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;

class NetworkUtil {
// next three lines makes this class a Singleton
static NetworkUtil _instance = new NetworkUtil.internal();
NetworkUtil.internal();
factory NetworkUtil() => _instance;

final JsonDecoder _decoder = new JsonDecoder();

Future<dynamic> get(String url) {
return http.get(url).then((http.Response response) {
final String res = response.body;
final int statusCode = response.statusCode;

if (statusCode < 200 || statusCode > 400 || json == null) {
throw new Exception("Error while fetching data");
}
return _decoder.convert(res);
});
}

Future<dynamic> post(String url, {Map headers, body, encoding}) {
return http
.post(url, body: body, headers: headers, encoding: encoding)
.then((http.Response response) {
final String res = response.body;
final int statusCode = response.statusCode;

if (statusCode < 200 || statusCode > 400 || json == null) {
throw new Exception("Error while fetching data");
}
return _decoder.convert(res);
});
}
}

We need a network util class that can wrap get and post requests plus handle encoding / decoding of JSONs. Notice how get and post return Future’s, this is exactly why Dart is beautiful. Asynchrony built-in!

data/rest_ds.dart

import 'dart:async';

import 'package:login_app/utils/network_util.dart';
import 'package:login_app/models/user.dart';

class RestDatasource {
NetworkUtil _netUtil = new NetworkUtil();
static final BASE_URL = "http://YOUR_BACKEND_IP/login_app_backend";
static final LOGIN_URL = BASE_URL + "/login.php";
static final _API_KEY = "somerandomkey";

Future<User> login(String username, String password) {
return _netUtil.post(LOGIN_URL, body: {
"token": _API_KEY,
"username": username,
"password": password
}).then((dynamic res) {
print(res.toString());
if(res["error"]) throw new Exception(res["error_msg"]);
return new User.map(res["user"]);
});
}
}

RestDatasource is a data source that uses a rest backend for login_app, which I assume you already have. ‘/login’ route expects three parameters: username, password and a token key (skip this to keep things simple).

JSON response format if there’s an error:

{ error: true, error_msg: “Invalid credentitals”}

JSON response format if there’s no error:

{ error: false, user: { username: “Some username”, password: “Some password” } }

models/user.dart

class User {
String _username;
String _password;
User(this._usn, this._password);

User.map(dynamic obj) {
this._username = obj["username"];
this._password = obj["password"];
}

String get username => _username;
String get password => _password;

Map<String, dynamic> toMap() {
var map = new Map<String, dynamic>();
map["username"] = _username;
map["password"] = _password;

return map;
}
}

We use a model class to define a user. Also note, how we use a constructor to create a new User object out of a dynamic map object. I won’t get into specifics of Dart here since that’s not the point of this post.

data/database_helper.dart

import 'dart:async';
import 'dart:io' as io;

import 'package:path/path.dart';
import 'package:login_app/models/user.dart';
import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';

class DatabaseHelper {
static final DatabaseHelper _instance = new DatabaseHelper.internal();
factory DatabaseHelper() => _instance;

static Database _db;

Future<Database> get db async {
if(_db != null)
return _db;
_db = await initDb();
return _db;
}

DatabaseHelper.internal();

initDb() async {
io.Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, "main.db");
var theDb = await openDatabase(path, version: 1, onCreate: _onCreate);
return theDb;
}


void _onCreate(Database db, int version) async {
// When creating the db, create the table
await db.execute(
"CREATE TABLE User(id INTEGER PRIMARY KEY, username TEXT, password TEXT)");
print("Created tables");
}

Future<int> saveUser(User user) async {
var dbClient = await db;
int res = await dbClient.insert("User", user.toMap());
return res;
}

Future<int> deleteUsers() async {
var dbClient = await db;
int res = await dbClient.delete("User");
return res;
}

Future<bool> isLoggedIn() async {
var dbClient = await db;
var res = await dbClient.query("User");
return res.length > 0? true: false;
}

}

The above class uses SQFLite plugin for Flutter to handle insertion and deletion of User credentials to the database. Note: Dart has inbuilt support for factory constructor, this means you can easily create a singleton without much ceremony.

auth.dart

import 'package:login_app/data/database_helper.dart';

enum AuthState{ LOGGED_IN, LOGGED_OUT }

abstract class AuthStateListener {
void onAuthStateChanged(AuthState state);
}

// A naive implementation of Observer/Subscriber Pattern. Will do for now.
class AuthStateProvider {
static final AuthStateProvider _instance = new AuthStateProvider.internal();

List<AuthStateListener> _subscribers;

factory AuthStateProvider() => _instance;
AuthStateProvider.internal() {
_subscribers = new List<AuthStateListener>();
initState();
}

void initState() async {
var db = new DatabaseHelper();
var isLoggedIn = await db.isLoggedIn();
if(isLoggedIn)
notify(AuthState.LOGGED_IN);
else
notify(AuthState.LOGGED_OUT);
}

void subscribe(AuthStateListener listener) {
_subscribers.add(listener);
}

void dispose(AuthStateListener listener) {
for(var l in _subscribers) {
if(l == listener)
_subscribers.remove(l);
}
}

void notify(AuthState state) {
_subscribers.forEach((AuthStateListener s) => s.onAuthStateChanged(state));
}
}

auth.dart defines a Broadcaster/Observable kind of object that can notify its Subscribers of any change in AuthState (logged_in or not). You should use something like Redux to manage these kind of global states in your app. But I didn’t want to use Redux for this simple app and hence I chose to implement it manually.

screens/login/login_screen_presenter.dart

import 'package:login_app/data/rest_ds.dart';
import 'package:login_app/models/user.dart';

abstract class LoginScreenContract {
void onLoginSuccess(User user);
void onLoginError(String errorTxt);
}

class LoginScreenPresenter {
LoginScreenContract _view;
RestDatasource api = new RestDatasource();
LoginScreenPresenter(this._view);

doLogin(String username, String password) {
api.login(username, password).then((User user) {
_view.onLoginSuccess(user);
}).catchError((Exception error) => _view.onLoginError(error.toString()));
}
}

login_screen_presenter.dart defines an interface for LoginScreen view and a presenter that incorporates all business logic specific to login screen itself. Thanks to MVP!

We only need to handle the login logic here since we do not any other feature in LoginScreen for now.

screens/login/login_screen.dart

import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:login_app/auth.dart';
import 'package:login_app/data/database_helper.dart';
import 'package:login_app/models/user.dart';
import 'package:login_app/screens/login/login_screen_presenter.dart';

class LoginScreen extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return new LoginScreenState();
}
}

class LoginScreenState extends State<LoginScreen>
implements LoginScreenContract, AuthStateListener {
BuildContext _ctx;

bool _isLoading = false;
final formKey = new GlobalKey<FormState>();
final scaffoldKey = new GlobalKey<ScaffoldState>();
String _password, _password;

LoginScreenPresenter _presenter;

LoginScreenState() {
_presenter = new LoginScreenPresenter(this);
var authStateProvider = new AuthStateProvider();
authStateProvider.subscribe(this);
}

void _submit() {
final form = formKey.currentState;

if (form.validate()) {
setState(() => _isLoading = true);
form.save();
_presenter.doLogin(_username, _password);
}
}

void _showSnackBar(String text) {
scaffoldKey.currentState
.showSnackBar(new SnackBar(content: new Text(text)));
}

@override
onAuthStateChanged(AuthState state) {

if(state == AuthState.LOGGED_IN)
Navigator.of(_ctx).pushReplacementNamed("/home");
}

@override
Widget build(BuildContext context) {
_ctx = context;
var loginBtn = new RaisedButton(
onPressed: _submit,
child: new Text("LOGIN"),
color: Colors.primaries[0],
);
var loginForm = new Column(
children: <Widget>[
new Text(
"Login App",
textScaleFactor: 2.0,
),
new Form(
key: formKey,
child: new Column(
children: <Widget>[
new Padding(
padding: const EdgeInsets.all(8.0),
child: new TextFormField(
onSaved: (val) => _username = val,
validator: (val) {
return val.length < 10
? "Username must have atleast 10 chars"
: null;
},
decoration: new InputDecoration(labelText: "Username"),
),
),
new Padding(
padding: const EdgeInsets.all(8.0),
child: new TextFormField(
onSaved: (val) => _password = val,
decoration: new InputDecoration(labelText: "Password"),
),
),
],
),
),
_isLoading ? new CircularProgressIndicator() : loginBtn
],
crossAxisAlignment: CrossAxisAlignment.center,
);

return new Scaffold(
appBar: null,
key: scaffoldKey,
body: new Container(
decoration: new BoxDecoration(
image: new DecorationImage(
image: new AssetImage("assets/login_background.jpg"),
fit: BoxFit.cover),
),
child: new Center(
child: new ClipRect(
child: new BackdropFilter(
filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
child: new Container(
child: loginForm,
height: 300.0,
width: 300.0,
decoration: new BoxDecoration(
color: Colors.grey.shade200.withOpacity(0.5)),
),
),
),
),
),
);
}

@override
void onLoginError(String errorTxt) {
_showSnackBar(errorTxt);
setState(() => _isLoading = false);
}

@override
void onLoginSuccess(User user) async {
_showSnackBar(user.toString());
setState(() => _isLoading = false);
var db = new DatabaseHelper();
await db.saveUser(user);
var authStateProvider = new AuthStateProvider();
authStateProvider.notify(AuthState.LOGGED_IN);
}
}

There are a lot of things happening here. LoginScreen is a StatefulWidget since we want to store the state for entered username and password. WE define a Form widget that will make our life easier in handling form validations. May not be necessary here, but hey why not use something different!?.

I’ve also used nice fancy Backdrop effect to make things more interesting. All thanks to this stackoverflow answer.

We use a GlobalKey to manage FormState to see if the form is valid or not when the user clicks on login. Similary we need a GlobalKey for Scaffold if we want to show a Snackbar feedback after login.

LoginState implements LoginScreenContract which has methods for onLoginSuccess and onLoginError. These methods will be called by the Presenter.

LoginState also listens for any change in AuthState. So if the user logs in, LoginState will be notified of it. Here it will simply use Navigator class to replace the current route with /home.

screens/home/home_screen.dart

import 'package:flutter/material.dart';

class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: new AppBar(title: new Text("Home"),),
body: new Center(
child: new Text("Welcome home!"),
),
);
}

}

Finally, edit pubspec.yaml to include the necessary plugins.

name: login_app
description: Some login app description here.

dependencies:
flutter:
sdk:
flutter

# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.0

sqflite: ^0.7.1
path_provider: ^0.3.1

dev_dependencies:
flutter_test:
sdk:
flutter


# For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec

# The following section is specific to Flutter.
flutter:

# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true

# To add assets to your application, add an assets section, like this:
assets:
- assets/login_background.jpg

Note: Add your asset files to assets/ folder in the root of your project. I have used a background image for login screen. This should also be added in your pubspec.yaml file for Flutter to recognize it as an asset.

Conclusion

We’ve built a good enough login app from scratch. This should give you some insight on how to handle REST requests and create a database client. Hope you have liked this article. I’d be glad to answer any queries in comments!

Thank you!