Implementing Auth in Flutter using Supabase and Getx
Aditya Subrahmanya Bhat
Posted on July 26, 2021
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.
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.
Step 2. Go to Supabase and click on Start Project.
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.
Step 5. Enter your credentials and click on Sign In.
Step 6. Click on New Project.
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.
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.
Step 9. Once it is ready , go to settings and then API section.
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.
Step 11. Go to the table editor section and click on Create a new table.
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.
Step 13. Now click on the "+" icon beside the id column to create a new column.
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.
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 :
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';classAuthService{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 databaseFuture<GotrueSessionResponse>signUpUser(Stringname,Stringemail,Stringpassword)async{finaluser=await_authClient.auth.signUp(email,password);finalresponse=await_authClient.from('Users').insert([{"Id":user.user.id,"Name":name,"Email":email}]).execute();if(response.error==null){returnuser;}}
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.
{"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 userFuture<GotrueSessionResponse>signIn(Stringemail,Stringpassword)async{finaluser=await_authClient.auth.signIn(email:email,password:password);if(user.error==null){returnuser;}}
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 dataUsergetCurrentUser(){return_authClient.auth.user();}
The user() function returns user data of type User if there is a currently logged in user.
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.
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';classLoginPageextendsStatefulWidget{Stringemail='';LoginPage({this.email});@override_LoginPageStatecreateState()=>_LoginPageState();}class_LoginPageStateextendsState<LoginPage>{AuthService_service=AuthService();final_emailController=TextEditingController();final_passwordController=TextEditingController();final_formKey=GlobalKey<FormState>();boollogging=false,obscure=true;@overrideWidgetbuild(BuildContextcontext){finalsize=MediaQuery.of(context).size;_emailController.text=widget.email;returnScaffold(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),],),),),),],),),),);}Futurelogin()async{}}SnackBarsnackBar({Stringcontent,Stringtype})=>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';classRegisterPageextendsStatefulWidget{@override_RegisterPageStatecreateState()=>_RegisterPageState();}class_RegisterPageStateextendsState<RegisterPage>{AuthService_service=AuthService();final_emailController=TextEditingController();final_passwordController=TextEditingController();final_nameController=TextEditingController();final_formKey=GlobalKey<FormState>();boolregistering=false;boolobscure=true;@overrideWidgetbuild(BuildContextcontext){finalsize=MediaQuery.of(context).size;returnScaffold(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),],),),),),],),),),);}Futureregister()async{}SnackBarsnackBar({Stringcontent,Stringtype})=>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
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
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 :
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.
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.
For help getting started with Flutter, view our
online documentation, which offers tutorials,
samples, guidance on mobile development, and a full API reference.