Authenticating Flutter Application Using Laravel API and Passport
In this article, I will be walking you through basic authentication for your flutter application using a Laravel Backend.
Prerequisite
- Know how to set up a laravel environment,
- Have a flutter environment setup
- Know how to create a flutter starter app
- Read and complete the tutorial Laravel 6 API Authentication with Laravel Passport. It will help in setting up the Backend Service used for this tutorial.
- Complete the tutorial on No 4. and keep the Laravel server running
why flutter?
Flutter is Google’s UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase.
See my presentation, explaining the nativity of flutter https://speakerdeck.com/godilite/the-nativity-of-flutter
First, let’s be on the same page with our Laravel Endpoints.
Setup Laravel backend
Take few minutes, read this detailed article I wrote specially for building and authenticating Laravel Rest API with Passport. Once you are ready, move to the next step.
Let’s Flutter on Dart
Create a Flutter application
You can now create a fresh flutter application, of course, you can use any code editor as recommended by flutter.
Install Flutter packages
We will need the following flutter packages:
- http package, to consume HTTP resources.
- shared_preferences package, providing a persistent store for simple data.
Add the packages to your pubspec.yaml
file
dependencies:
shared_preferences: ^0.5.6+1
http: ^0.12.0+4
Run the command flutter pub get
Note: You could also choose to handle data storage with SQLite or Hive packages especially for large data structures.
Folder structure
Create your folder structure like so:
Within the lib
directory, create two folders:
network_utils
andscreen
respectively.
In screen
directory, create login.dart, home.dart, register.dart
and createapi.dart
file in network_utils
folder.
network_utils/api.dart
In the api.dart
file, write a Network class that manages all API methods
Within the Network
class, define String _url = 'http://localhost:8000/api/v1
this is basically the BASE_URL for our Laravel API.
If you are using an android emulator then change localhost:8000
to 10.0.2.2:8000
Also, define a variable token,
this will store API token for authentication and would be attached to every request that requires authentication.
Next, we write an asynchronous function _getToken
_getToken()async{
SharedPreferences localStorage = await SharedPreferences.getInstance();
token = jsonDecode(localStorage.getString('token'))['token'];
}
This function checks for token stored in the user device and assigns it to the initially defined String token;
We used SharedPreferences
to allow us to get an instance of the device's localStorage
where we use the _getString
method to retrieve a key token
stored during login or registration. Wrap this method with jsonDecode
to convert the key/value pair retrieved into an array. This way, we can access the value ['token']
.
The authData()
function is an asynchronous function that handles all post
request to our login and register API Endpoint, it takes two arguments, data
and apiUrl
.
We build a fullUrl
by appending the baseUrl _url
and the apiUrl
passed to the function, the http.post()
method takes the fullUrl, body, and headers to make the POST
request. The authData
function returns the response from this request.
You may have observed that for the headers
parameter, we called _setHeaders()
where pass in our headers like so:
_setHeaders() => {
'Content-type' : 'application/json',
'Accept' : 'application/json',
'Authorization' : 'Bearer $token'
};
This makes it reusable and cleaner since we would have to call it here and there while making requests.
Next, getData()
function, this is also an async
function that takes an argument apiUrl
, you can add more arguments say data
if your query requires passing a set of parameters for instance. For the purpose of this article, our GET
request takes no extra parameters.
In the getData()
function, we build the fullUrl similar to the postData
and call the _getToken()
to ensure that token
is set otherwise it will be null, and our endpoint will return unauthorized
we didn’t do this for the authData()
method because the register and login routes do not require authentication.
The http.get()
method helps in sending our GET
request, here, no body
parameter is passed.
main.dart
This is the main entry point to our application, here we will check if the user is authenticated and return either the login screen or the home screen.
Within the _CheckAuthState
we define a boolean isAuth = false;
in the initState()
we call _checkIfLoggedIn
this method checks localStorage
for availability of token
and sets isAuth = true
like so:
void _checkIfLoggedIn() async{
SharedPreferences localStorage = await SharedPreferences.getInstance();
var token = localStorage.getString('token');
if(token != null){
setState(() {
isAuth = true;
});
}
}
Lastly, in the build method, we check if isAuth = true
and return Home()
else Login()
We are here, if you’ve got no errors so far, Lets Register and Login.
login.dart
In the Login class, we start by building a basic login form with input validation using the Form
Widget.
you can copy and paste from the code snippet above or build a form for yourself. I will explain a few necessary parts of the code.
Define a Boolean _isLoading = false;
we’ll use this to change the state of the login
button to processing...
Also, define email
and password
variables, set their values from their respective form fields after validation. we’ll define a _formKey
and a _scaffoldKey
as follows
final _formKey = GlobalKey<FormState>();This uniquely identifies the Form, and allows validation of the form in a later step.final _scaffoldKey = GlobalKey<ScaffoldState>();Assign a GlobalKey to the Scaffold, then use the key.currentState property to obtain the ScaffoldState for a snackBar later.
Now, just below the build widget, we write the async _login()
method like so:
void _login() async{
setState(() {
_isLoading = true;
});
var data = {
'email' : email,
'password' : password
};
var res = await Network().authData(data, '/login');
var body = json.decode(res.body);
if(body['success']){
SharedPreferences localStorage = await SharedPreferences.getInstance();
localStorage.setString('token', json.encode(body['token']));
localStorage.setString('user', json.encode(body['user']));
Navigator.push(
context,
new MaterialPageRoute(
builder: (context) => Home()
),
);
}else{
_showMsg(body['message']);
}
setState(() {
_isLoading = false;
});
}
We set the _isLoading = true
to change the login
button to processing...
Collect the form values and build a data
Object which will be pass to our API call.
Define a var res = await Network().authData(data, '/login)
Here, we make an API call to authData()
method which we initially created at api.dart
file within the Network
class, remember it takes two parameters right? Hence, we pass in our data
and API route '/login’
.
Once there is a response from our backend service, it will be stored in the res
variable, we then decode this JSON res.body
into var body = json.decode(res.body)
Using an if conditional statement, we check for body['success']
this is a Boolean from our API, if it's true, then we have successfully authenticated into our application, hence, we set localStorage
values for token
and user
using SharedPreferences as shown in the snippets. And using Navigator.push
we navigate to the Home()
route.
Else, if there is an error from our endpoint, we call the _showMsg()
and pass in the message body['message']
this triggers a snackBar notification with the error message.
And Lastly, we set the _isLoading = false
.
register.dart
The register.dart is similar to login.dart, significant change is in the number of form fields, and in the API endpoint '/register'
as seen below:
Take a minute and go through the async_register()
method, then we move!
lib/main.dart
Here, we will retrieve user data stored in localStorage, and also implement logout action.
At the initState()
method, we call _loadUserData
which returns the user data stored in localStorage during register/login,
_loadUserData() async{
SharedPreferences localStorage = await SharedPreferences.getInstance();
var user = jsonDecode(localStorage.getString('user'));
if(user != null) {
setState(() {
name = user['fname'];
});
}
}
You can set this user data to any variable declared to hold user data, I set user['fname']
to a String name
, this is later used in the Text
widget on line 40,
Text('Hi, $name',
style: TextStyle(
fontWeight: FontWeight.bold
),
),
Next, we create a logout button, using a RaisedButton
widget, and onPressed
we call the logout()
method.
void logout() async{
var res = await Network().getData('/logout');
var body = json.decode(res.body);
if(body['success']){
SharedPreferences localStorage = await SharedPreferences.getInstance();
localStorage.remove('user');
localStorage.remove('token');
Navigator.push(
context,
MaterialPageRoute(builder: (context)=>Login()));
}
}
The logout method calls the getData
method of our Network
class and passes the apiUrl /logout
once we receive a success response, indicating that the token has been invalidated by our backend passport service, we call .remove()
method on our localStorage which is an instance of SharedPreferences, we pass in the token
key and do same for user
and Navigate to the Login()
screen.
Run application
Run your application on an emulator device or host your laravel backend on heroku to properly test from an external device. Ensure edit your base URL in any case.
If you’ve successfully reached this point, I want to say,
Thank You! and Well Done!
Here is my git repository for the flutter application
Having issues? Do let me know.