Implementing Auth in Flutter using Supabase and Getx

Implementing Auth in Flutter using Supabase and Getx

Hey there!!!

Have you ever needed a backend service like Firebase for your Flutter app or website but didn't want to go through all the complex setup procedures of Firebase? Or have you felt like using a different backend service just because you're bored of using Firebase 😂?

Well , here comes the hero. Superman to the rescue!!!!

Oh wait , it's not Superman 😬, it's Supabase.

What is Supabase?

Supabase is an open-source backend-as-a-service developed as an alternative to Firebase. It is built using many other open-source packages and tools and it offers a lot of features that a developer needs for his app or web application. It has a relational database (PostgreSQL database) , built-in authentication and authorization , storage , real-time subscriptions, and much more!

A relational database is one that stores data which have some relationships between them

Supabase is currently in public beta , and there are more features and functions to come when it goes public or when it is production-ready. Supabase has one of the best documentation out there. Check out their website.

What we're going to do

We'll create a flutter app and set up authentication in it using Supabase. In this app, we'll also be using Get.

Get is a package used for state management , route management, and dependency injection in flutter. It's quite easy to understand and get started with it.

State management - Apps and websites have something called state. Whenever a user interacts with the app , the state of the app changes(in simple words , the app reacts to the user's action) . This state needs to be managed to define how and when it should change. This is done using the state management technique. Flutter comes with a built-in state management technique - setstate.

Route management - Sometimes we may need to show different screens to the user , this is done using route management.

Dependency Injection - Some objects in the app depend on another for its functioning , this is called dependency. To give the object what it needs is dependency injection(It's like passing a service to a client). With this , the object can be accessed anywhere in the widget tree easily.

Getting Started

Step 1. Create a flutter app.
Step 2. Go to Supabase and click on Start Project.

Screenshot (357).png Step 3. If you are new to Supabase , it'll take you to the sign-in page. (If you already have signed in , skip to Step 6).
Step 4. Click on Continue with Github.

Screenshot (358).png Step 5. Enter your credentials and click on Sign In.

Screenshot (359).png Step 6. Click on New Project. Screenshot (361).png Step 7. Give the project some name(I'll be naming it Auth) and type in a strong password(now , remember to remember the password), and then select the closest server region to your location.

Screenshot (362).png Step 8. Just sit back and enjoy a cup of coffee while Supabase creates your project for you.

Meanwhile you can check out their documentation and API references on their website.

Screenshot (363).png Step 9. Once it is ready , go to settings and then API section.

oie_HGC1zqysxFFI.jpg Step 10. Note down your project URL and project API key , we will be needing them in our app. That's all we need to set up a backend service for our app.No other procedures involving the editing of build.gradle files etc like Firebase.

Now go the Authentication section and then into settings and disable email confirmations or else we'll have to verify each email before it we sign In.

Screenshot (372).png

Step 11. Go to the table editor section and click on Create a new table.

Screenshot (364).png

Step 12. Let's name it Users and leave the rest to default and click on Save. We will be using this table to store the user data of registered users.

Screenshot (367).png

Step 13. Now click on the "+" icon beside the id column to create a new column.

Screenshot (369).png

We'll name it Name and set the type to text and unselect Allow nullable because we don't want the name of the user to be null by accident.

Step 14. We'll create 2 more columns Email and Id to store email and user Id.

Screenshot (370).png

Screenshot (371).png

That's it , now we have our database ready!!.

Building the app

Let's open the app folder and go to pubspec.yaml and import the following packages :

get_storage - Now let's assume that a user logs in to the app and uses it for some time and then exits the app. The next time he opens the app , it shouldn't take him to the login page again, right? So , we need to store a session string for the user so that the app takes him to the home page. This is done using this package.

Now , go to main.dart and paste the following code :

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Wrapper(),
    );
  }
}

class Wrapper extends StatefulWidget {
  @override
  WrapperState createState() => WrapperState();
}

class WrapperState extends State<Wrapper> {
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: CircularProgressIndicator(),
      ),
    );
  }
}

The Wrapper class is used to listen to auth changes(whether the user is logged in or not) in the app and take him to the appropriate page.

Create 2 global variables and assign them the values you copied earlier.

final String _supaBaseUrl = 'Project URL';
final String _supaBaseKey = 'Project API key';

Import these in main.dart

import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:supabase/supabase.dart';

Now create a dependency in void main

void main() {
  Get.put<SupabaseClient>(SupabaseClient(_supaBaseUrl, _supaBaseKey));
  Get.put<GetStorage>(GetStorage());
  runApp(MyApp());
}

We create a SupabaseClient dependency to access all the functions of supabase. Get.put() takes in a dependency to inject as an argument. We specify the type of dependency using less than and greater than operators.

SupabaseClient takes in 2 arguments - project URL and project API key.

We will also create a dependency for storing the session string and the type will be GetStorage.

Now , let's create a new file inside lib folder and name it authService.dart. We will be implementing the authentication functions of our app in a separate class called AuthService. Paste the following code in it :

import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:supabase/supabase.dart';


class AuthService {
final _authClient=Get.find<SupabaseClient>();
//register user and create a custom user data in the database

//log in user

//get currently logged in user data

//logOut

//RecoverSession

}

Get.find() finds the injected dependency instance in the whole widget tree and returns it. In our case, it is located in main.dart. We store it in a variable and use it later.

Register user and create user data

//register user and create a custom user data in the database
Future<GotrueSessionResponse> signUpUser(String name, String email, String password) async {
    final user =
        await _authClient.auth.signUp(email, password);
    final response =
        await _authClient.from('Users').insert([
      {"Id": user.user.id, "Name": name, "Email": email}
    ]).execute();
    if (response.error == null) {
      return user;
    }
  }

We create a function signUpUser() which returns a Future of type GotrueSessionResponse.
Future - Sometimes we need to retrieve data from the database or somewhere else where the data may not be readily available or may take some time to load depending upon your internet connection. Such data are called Futures.To use them in our code , we need to mark that part of code async(meaning asynchronous) and use the keyword await to await the data to arrive. When we use await in our code , whatever code comes after that await code line is executed only after the data arrives(or after the awaited code is completely executed).

To register the user , we use the _authClient variable and tap into the auth property and then use the signUp() function.It takes 2 arguments - email and password.Since it return a Future of type GotrueSessionResponse , we use await.

To create user data in our database, we use the _authClient variable and use the from() function which takes in the database name as an argument, and use the insert function which takes a list of Maps as an argument. Finally, we execute it.

Syntax : _authClient.from("Database name").insert([
{"Column_1_name":value, "Column_2_name":value , and so on}, {"Column_1_name":value, "Column_2_name":value , and so on},
and so on
]).execute();

Log In User

//log in user
  Future<GotrueSessionResponse> signIn(String email, String password) async {
    final user =
        await _authClient.auth.signIn(email: email, password: password);
    if (user.error == null) {
      return user;
    }
  }

To log in the user , we use the _authClient variable and tap into the auth property and then use the signIn() function.It takes 2 named arguments - email and password.Since it return a Future of type GotrueSessionResponse , we use await.

Get current user

//get currently logged in user data
User getCurrentUser() {
    return _authClient.auth.user();
  }

The user() function returns user data of type User if there is a currently logged in user.

Log out user

//logOut
Future<GotrueResponse> logOut() async {
    await _authClient.auth.signOut();
  }

The signOut() function simply signs out the current user(if there is a logged-in user) and returns a Future of type GotrueResponse. It takes no arguments.

Recover user session

//RecoverSession
  Future<GotrueSessionResponse> recoverSession(String session) async {
    return await _authClient.auth.recoverSession(session);
  }

This function is to recover user session if a user has logged in , used the app for some time, and exited. It takes a String as an argument and returns a Future of type GotrueSessionResponse.

UI

Now it's time to add some makeup to our app and make it look beautiful.
Go to lib folder and create a file called loginPage.dart and paste the following code :

import 'package:supabase_auth/Screens/Auth/registerPage.dart';
import 'package:supabase_auth/Screens/Home/home.dart';
import 'package:supabase_auth/Services/authService.dart';
import 'package:flutter/material.dart';
import 'package:form_field_validator/form_field_validator.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';

class LoginPage extends StatefulWidget {
  String email = '';
  LoginPage({this.email});
  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  AuthService _service = AuthService();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _formKey = GlobalKey<FormState>();
  bool logging = false, obscure = true;

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    _emailController.text = widget.email;
    return Scaffold(
      body: SingleChildScrollView(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Padding(
                padding: EdgeInsets.only(top: 100.0),
                child: Center(
                  child: Text(
                    'LOGIN',
                    style: TextStyle(
                      color: Colors.black,
                      fontSize: 50,
                    ),
                  ),
                ),
              ),
              Container(
                margin: EdgeInsets.only(
                  top: size.height / 6,
                  left: 40.0,
                  right: 40.0,
                ),
                decoration: BoxDecoration(
                  color: Colors.blue,
                  borderRadius: BorderRadius.circular(20.0),
                ),
                child: Padding(
                  padding: EdgeInsets.only(
                    top: 20.0,
                    left: 20.0,
                    right: 20.0,
                  ),
                  child: Form(
                    key: _formKey,
                    child: Column(
                      children: [
                        TextFormField(
                          controller: _emailController,
                          decoration: InputDecoration(
                            hintText: "Email",
                            hintStyle: TextStyle(color: Colors.white),
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(20.0),
                            ),
                            focusedBorder: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(20.0),
                            ),
                          ),
                          validator: MultiValidator([
                            RequiredValidator(errorText: "Required"),
                            EmailValidator(
                                errorText:
                                    "Please enter a valid email address"),
                          ]),
                        ),
                        SizedBox(
                          height: 20.0,
                        ),
                        TextFormField(
                          obscureText: true,
                          controller: _passwordController,
                          decoration: InputDecoration(
                            hintText: "Password",
                            hintStyle: TextStyle(color: Colors.white),
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(20.0),
                            ),
                            focusedBorder: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(20.0),
                            ),
                          ),
                          validator: MultiValidator([
                            RequiredValidator(errorText: "Required"),
                            MinLengthValidator(6,
                                errorText:
                                    "Password must contain atleast 6 characters"),
                            MaxLengthValidator(20,
                                errorText:
                                    "Password must not be more than 20 characters"),
                          ]),
                        ),
                        SizedBox(
                          height: 20.0,
                        ),
                        logging == false
                            ? ElevatedButton(
                                onPressed: () async {
                                  if (_formKey.currentState.validate()) {
                                    setState(() {
                                      logging = true;
                                    });
                                    login();
                                  }
                                },
                                child: Padding(
                                  padding:
                                      EdgeInsets.symmetric(horizontal: 50.0),
                                  child: Text(
                                    'Login',
                                    style: TextStyle(color: Colors.black),
                                  ),
                                ),
                                style: ButtonStyle(
                                  shape: MaterialStateProperty.all(
                                    RoundedRectangleBorder(
                                      borderRadius: BorderRadius.circular(20.0),
                                    ),
                                  ),
                                  backgroundColor:
                                      MaterialStateProperty.all(Colors.white),
                                ),
                              )
                            : CircularProgressIndicator(
                                valueColor:
                                    AlwaysStoppedAnimation<Color>(Colors.black),
                              ),
                        SizedBox(
                          height: 20.0,
                        ),
                        Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Text(
                              "Don't have an account? ",
                              style: TextStyle(color: Colors.white),
                            ),
                            InkWell(
                              onTap: () {
                                Navigator.pushReplacement(
                                  context,
                                  MaterialPageRoute(
                                    builder: (_) => RegisterPage(),
                                  ),
                                );
                              },
                              child: Text(
                                "Register",
                                style: TextStyle(
                                    color: Colors.black,
                                    fontWeight: FontWeight.bold),
                              ),
                            ),
                          ],
                        ),
                        SizedBox(height: 20.0),
                      ],
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Future login() async {}
}

SnackBar snackBar({String content, String type}) => SnackBar(
      content: Text(
        content,
        style: TextStyle(
          color: Colors.white,
          fontSize: 20.0,
        ),
      ),
      backgroundColor: type == "Error" ? Colors.red : Colors.green,
    );
}

Go to the lib folder and create a file called registerPage.dart and paste the following code :

import 'package:supabase_auth/Screens/Auth/loginPage.dart';
import 'package:supabase_auth/Services/authService.dart';
import 'package:flutter/material.dart';
import 'package:form_field_validator/form_field_validator.dart';

class RegisterPage extends StatefulWidget {
  @override
  _RegisterPageState createState() => _RegisterPageState();
}

class _RegisterPageState extends State<RegisterPage> {
  AuthService _service = AuthService();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  final _nameController = TextEditingController();
  final _formKey = GlobalKey<FormState>();
  bool registering = false;
  bool obscure = true;

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;

    return Scaffold(
      body: SingleChildScrollView(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Padding(
                padding: EdgeInsets.only(top: 100.0),
                child: Center(
                  child: Text(
                    'REGISTER',
                    style: TextStyle(
                      color: Colors.black,
                      fontSize: 50,
                    ),
                  ),
                ),
              ),
              Container(
                margin: EdgeInsets.only(
                  top: size.height / 6,
                  left: 40.0,
                  right: 40.0,
                ),
                decoration: BoxDecoration(
                  color: Colors.blue,
                  borderRadius: BorderRadius.circular(20.0),
                ),
                child: Padding(
                  padding: EdgeInsets.only(
                    top: 20.0,
                    left: 20.0,
                    right: 20.0,
                  ),
                  child: Form(
                    key: _formKey,
                    child: Column(
                      children: [
                        TextFormField(
                          controller: _nameController,
                          decoration: InputDecoration(
                            hintText: "Name",
                            hintStyle: TextStyle(color: Colors.white),
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(20.0),
                            ),
                            focusedBorder: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(20.0),
                            ),
                          ),
                          validator: MultiValidator([
                            RequiredValidator(errorText: "Required"),
                          ]),
                        ),
                        SizedBox(
                          height: 20.0,
                        ),
                        TextFormField(
                          controller: _emailController,
                          decoration: InputDecoration(
                            hintText: "Email",
                            hintStyle: TextStyle(color: Colors.white),
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(20.0),
                            ),
                            focusedBorder: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(20.0),
                            ),
                          ),
                          validator: MultiValidator([
                            RequiredValidator(errorText: "Required"),
                            EmailValidator(
                                errorText:
                                    "Please enter a valid email address"),
                          ]),
                        ),
                        SizedBox(
                          height: 20.0,
                        ),
                        TextFormField(
                          obscureText: true,
                          controller: _passwordController,
                          decoration: InputDecoration(
                            hintText: "Password",
                            hintStyle: TextStyle(color: Colors.white),
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(20.0),
                            ),
                            focusedBorder: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(20.0),
                            ),
                          ),
                          validator: MultiValidator([
                            RequiredValidator(errorText: "Required"),
                            MinLengthValidator(6,
                                errorText:
                                    "Password must contain atleast 6 characters"),
                            MaxLengthValidator(20,
                                errorText:
                                    "Password must not be more than 20 characters"),
                          ]),
                        ),
                        SizedBox(
                          height: 20.0,
                        ),
                        registering == false
                            ? ElevatedButton(
                                onPressed: () async {
                                  if (_formKey.currentState.validate()) {
                                    setState(() {
                                      registering = true;
                                    });
                                    register();
                                  }
                                },
                                child: Padding(
                                  padding:
                                      EdgeInsets.symmetric(horizontal: 50.0),
                                  child: Text(
                                    'Register',
                                    style: TextStyle(color: Colors.black),
                                  ),
                                ),
                                style: ButtonStyle(
                                  shape: MaterialStateProperty.all(
                                    RoundedRectangleBorder(
                                      borderRadius: BorderRadius.circular(20.0),
                                    ),
                                  ),
                                  backgroundColor:
                                      MaterialStateProperty.all(Colors.white),
                                ),
                              )
                            : CircularProgressIndicator(
                                valueColor:
                                    AlwaysStoppedAnimation<Color>(Colors.black),
                              ),
                        SizedBox(
                          height: 20.0,
                        ),
                        Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Text(
                              "Already have an account? ",
                              style: TextStyle(color: Colors.white),
                            ),
                            InkWell(
                              onTap: () {
                                Navigator.pushReplacement(
                                  context,
                                  MaterialPageRoute(
                                    builder: (_) => LoginPage(email: _emailController.text,),
                                  ),
                                );
                              },
                              child: Text(
                                "Login",
                                style: TextStyle(
                                    color: Colors.black,
                                    fontWeight: FontWeight.bold),
                              ),
                            ),
                          ],
                        ),
                        SizedBox(height: 20.0),
                      ],
                    ),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Future register() async { }

  SnackBar snackBar({String content, String type}) => SnackBar(
        content: Text(
          content,
          style: TextStyle(
            color: Colors.white,
            fontSize: 20.0,
          ),
        ),
        backgroundColor: type == "Error" ? Colors.red : Colors.green,
      );
}

Next go to main.dart and inside Wrapper widget , paste the following

void sessionCheck() async {
    await GetStorage.init();
    final box = Get.find<GetStorage>();
    AuthService _authService = AuthService();
    final session = box.read('user');
    if (session == null) {
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(
          builder: (context) => LoginPage(),
        ),
      );
    } else {
      final response = await _authService.recoverSession(session);
      await box.write('user', response.data.persistSessionString);
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(
          builder: (context) => HomePage(),
        ),
      );
    }
  }

We await GetStorage() to start/initialise the storage drive. Next, we find a GetStorage dependency instance and assign it to a variable called box.
We then use this box variable and call a function read() which reads the storage drive/container and checks if there is a value associated with the key we pass as an argument to it. The value will be a session string and we store it in a variable named session. Why so? Because it makes sense😂.
If there is no value present , then we take the user to the LoginPage.
Else if there is some value present , we recover that session by calling the recoverSession() function we had defined in the AuthService class. We call that function using an instance of AuthService class.
We store the returned value in a variable called sessionResponse.

Now we need to save this session again in the container so that we have access to it the next time the user opens the app.
We do it by awaiting box.write() which takes 2 arguments :
1.key = The name of the key where the value is stored. You can give it any name.
2.value = The session string to be stored in the container and it will be associated with the key we specify.
This function returns a Future of type void , so we await it. Once it is done , we go to the homePage.

Now that we have defined the function , we need to call it. Paste the following code in main.dart inside Wrapper widget

@override
  void initState() {
    // TODO: implement initState
    super.initState();
    sessionCheck();
  }

initState() is a function that is automatically called when the widget in which it is(In this case it is Wrapper widget), is loaded onto the stack. Or in simple words, it is called when the Wrapper widget loads/fires in the app.

Now go back to loginPage.dart and go the login function in it.

Suppose a user registers for the first time , we show him the LoginPage right? So , after login , his session string has to be saved so that it is available to the app the next time he opens it. We add two lines to the login function :

Future login() async {
    final box = Get.find<GetStorage>();
    final result =
        await _service.signIn(_emailController.text, _passwordController.text);

    if (result.data != null) {
      await box.write('user', result.data.persistSessionString);
      ScaffoldMessenger.of(context).showSnackBar(
        snackBar(content: "Login successful", type: "Success"),
      );
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(
          builder: (context) => HomePage(),
        ),
      );
    } else if (result.error?.message != null) {
      ScaffoldMessenger.of(context).showSnackBar(
        snackBar(content: result.error.message, type: "Error"),
      );
    }
  }

Go to registerPage.dart and implement the register function

  Future register() async {
    final result = await _service.signUpUser(
        _nameController.text, _emailController.text, _passwordController.text);
    if (result.data != null) {
      setState(() {
        registering = false;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        snackBar(content: "Registration Successful", type: "Success"),
      );
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(
          builder: (context) => LoginPage(
            email: _emailController.text,
          ),
        ),
      );
    } else if (result.error.message != null) {
      setState(() {
        registering = false;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        snackBar(content: result.error.message, type: "Error"),
      );
    }
  }

Home Page

We'll create a file named homePage.dart inside lib folder. We'll just create a simple home page with just a single button to implement the logout function.

import 'package:supabase_auth/Services/authService.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  AuthService _authservice = AuthService();
  bool loading = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: false,
        title: Text("Home Page"),
        centerTitle: true,
      ),
      body: Center(
        child: loading == false
            ? ElevatedButton(
                onPressed: () async {
                  setState(() {
                    loading = true;
                  });
                  await logout();
                },
                child: Text("Log Out"),
              )
            : CircularProgressIndicator(),
      ),
    );
  }

  Future logout() async { }
}

Log Out

Future logout() async {
    await _authservice.logOut();
    setState(() {
      loading = false;
    });
    Navigator.pushReplacement(
      context,
      MaterialPageRoute(
        builder: (context) => LoginPage(),
      ),
    );
  }

Just like earlier , we will use the AuthService class instance to call the logOut() function.
Since it returns a Future , we await it and once it is done , we take the user to the LoginPage.

Now go to authService.dart and go to the logOut function.

//logOut user
Future<GotrueResponse> logOut() async {
    Get.find<GetStorage>().remove('user');
    await _authClient.auth.signOut();
  }

Here we are removing the session string associated with the user because it is no longer needed once the user logs out. The remove() function removes the data from the container by key. It takes the key as an argument and returns a Future of type void.

Run the app

Hmmm , yeah that's it. Go ahead and run the app on your mobile or an emulator. Once you log in , you can check the database and see that your login details will be in the database/table we created.

Next Steps

Check out the full source code at github.

That's all folks , Thank you🙏