In this tutorial we're going to take the Flutter example that Supabase provides and rewrite it using VGV tooling, such as Very Good CLI, Flutter Bloc, layered architecture, and the most important thing, 100% test coverage π§ͺ.
This tutorial is based on the Quickstart: Flutter tutorial that Supabase has on their website. It will target Android, iOS, and the web.
Let's get started! π
β
But first, what is Supabase?
Supabase is an open source Firebase alternative. As a quick overview, Supabase offers you a bunch of services, like authentication with different providers (email, Apple, Azure, Discord, GitHub, GitLab, Bitbucket, Slack and much more), database (PostgreSQL), storage, functions, etc.
If you're looking to have more auth providers than the typical ones (email/password, Twitter, Facebook, Google, and Apple) or a SQL database instead of a NoSQL database, you could consider Supabase for your project. To learn more about Supabase, visit their official website here.
β
π‘Overview
These are the things that we are going to cover in this tutorial:
Login with email using deep linking.
Handle user status (authenticated or unauthenticated) to know where we need to navigate using Flow Builder.
Create a simple database with a script on the Supabase SQL Editor console.
Add rules to the database.
Update user information in the database.
β
Supabase initial configuration
Create a new project
The first step is to visit the Supabase website and create a login. When we have access to the dashboard, we can create a new organization and project.
Click on New Project and select your organization (if you don't have one, you should create a new one by clicking on New organization). The next step is to fill out the form to create a new project.
We can now see the new project on our dashboard.
Create a database with SQL Editor
To create a new database in our project, we should navigate to SQL Editor where we can create a script to create the table and the rules to apply in this database.
In this case, we are going to create a table called account. We need to modify the User Management Started script and add the following code:
-- Create a table for Public Account
create table account (
id uuid references auth.users not null,
username text unique,
companyName text,
primary key (id),
unique(username),
constraint username_length check (char_length(username) >= 3)
);
alter table account
enable row level security;
create policy "Public account are viewable by everyone." on account
for select using (true);
create policy "Users can insert their own account." on account
for insert with check (auth.uid() = id);
create policy "Users can update their own account." on account
for update using (auth.uid() = id);
-- Set up Realtime!
begin;
drop publication if exists supabase_realtime;
create publication supabase_realtime;
commit;
alter publication supabase_realtime
add table account;
Then, we need to click on RUN in the right bottom corner. Finally we have our new database created.
Supabase table editor
Once the database is created, we can navigate throughout the dashboard to view the table and rules.
In the left menu, we can navigate to table editor to see the account table. It is currently empty, but you can see that the table has three columns id, username and companyname.
If you want to check the rules that we created with the script, you can click on RLS enabled to see them.
Authentication configuration
As I mentioned at the beginning, the authentication method that we are going to use in this tutorial is email deep linking.
We need to add a redirect URL in our authentication dashboard (this is only if your application is on the web).
In this case, we are going to use the redirect URL that Supabase provides in the Flutter example. You should add the provider redirect URL if you are building a real project.
io.supabase.flutterquickstart://login-callback/
Now, we have everything set up and the next step is to create our Flutter project! π
β
Flutter project and packages
In this section we are going to create the Flutter project and different packages using Very Good CLI.
The first step is to activate Very Good CLI.
dart pub global activate very_good_cli
Once we have Very Good CLI activated, we can create the Flutter project. We just have to run the following line in the terminal.
very_good create very_good_supabase --desc "Example of a Flutter application using Very Good CLI and Supabase"
Which packages do we need for this project? π€
I've created a few of them under the package folder (you should create them in your application root).
Supabase auth client: this package is in charge of the sign-in and sign-out methods on Supabase.
Supabase database client: this package is in charge of get user information from the database and updating user information.
User repository: this repository is going to have the ability to use the SupabaseAuthClient to call the sign-in and sign-out methods, use the SupabaseDatabaseClient to get user profile information, and update the user on Supabase.
Form inputs: this package hosts the different form inputs to use on the view.
Email launcher: opens a default email app on Android and iOS.
You can visit the GitHub repository to see the code of the Form inputs and Email launcher packages. Here I'm only going to cover the packages related to Supabase.
The next step is to add the logic to this package. In this case, this package is going to be in charge of retrieving user information from the account table and also to updating the data on the account table.
First, we need to create a SupabaseUser model. To do that, we can create a new folder inside src called models. Then add the new supabase_user.dart file inside it. For this model I used the Json Serializable package.
/// {@template supabase_user}
/// Supabase user model
/// {@endtemplate}
@JsonSerializable()
class SupabaseUser extends Equatable {
/// {@macro supabase_user}
const SupabaseUser({
String? id,
required this.userName,
required this.companyName,
}) : id = id ?? '';
/// Connect the generated [_$SupabaseUserFromJson] function to the `fromJson`
/// factory.
factory SupabaseUser.fromJson(Map<String, dynamic> json) =>
_$SupabaseUserFromJson(json);
/// Id of the user.
final String id;
/// Name of the supabase user.
@JsonKey(name: 'username')
final String userName;
/// Company name of the supabase user.
@JsonKey(name: 'companyname')
final String companyName;
@override
List<Object> get props => [id, userName, companyName];
/// Empty Supabase object.
static const empty = SupabaseUser(
userName: '',
companyName: '',
);
/// Connect the generated [_$SupabaseUserToJson]
/// function to the `toJson` method.
Map<String, dynamic> toJson() => _$SupabaseUserToJson(this);
}
The next step is to add the logic in the supabase_database_client.dart file.
/// {@template supabase_database_client}
/// Supabase database client
/// {@endtemplate}
class SupabaseDatabaseClient {
/// {@macro supabase_database_client}
const SupabaseDatabaseClient({
required SupabaseClient supabaseClient,
}) : _supabaseClient = supabaseClient;
final SupabaseClient _supabaseClient;
/// Method to get the user information by id
/// from the profiles database on Supabase.
Future<SupabaseUser> getUserProfile() async {
try {
final response = await _supabaseClient
.from('account')
.select()
.eq('id', _supabaseClient.auth.currentUser?.id)
.single()
.execute();
final data = response.data as Map<String, dynamic<;
return SupabaseUser.fromJson(data);
} catch (error, stackTrace) {
Error.throwWithStackTrace(
SupabaseUserInformationFailure(error),
stackTrace,
);
}
}
/// Method to update the user information on the profiles database.
Future<void> updateUser({required SupabaseUser user}) async {
try {
final supabaseUser = SupabaseUser(
id: _supabaseClient.auth.currentUser?.id,
userName: user.userName,
companyName: user.companyName,
);
await _supabaseClient
.from('account')
.upsert(supabaseUser.toJson())
.execute();
} catch (error, stackTrace) {
Error.throwWithStackTrace(
SupabaseUpdateUserFailure(error),
stackTrace,
);
}
}
}
User repository
The first step is to create the package.
very_good create user_repository -t flutter_pkg --desc "A package which manages the user domain."
The second step is to modify the pubspec.yaml file to add the dependencies.
name: user_repository
description: A package which manages the user domain
publish_to: none
environment:
sdk: ">=2.17.0 <3.0.0"
dependencies:
equatable: ^2.0.3
flutter:
sdk: flutter
mocktail: ^0.3.0
supabase_auth_client:
path: ../supabase_auth_client
supabase_database_client:
path: ../supabase_database_client
supabase_flutter: ^0.3.1
dev_dependencies:
flutter_test:
sdk: flutter
very_good_analysis: ^3.0.1
Now we can add the logic for this package. In this repository we can use the SupabaseAuthClient and SupabaseDatabaseClient to be able to use the methods inside of each class. With this repository we can do the following:
Sign-in
Sign-out
Fetch User information
Update user
/// {@template user_repository}
/// A package which manages the user domain.
/// {@endtemplate}
class UserRepository {
/// {@macro user_repository}
UserRepository({
required SupabaseAuthClient authClient,
required SupabaseDatabaseClient databaseClient,
}) : _authClient = authClient,
_databaseClient = databaseClient;
final SupabaseAuthClient _authClient;
final SupabaseDatabaseClient _databaseClient;
/// Method to access the current user.
Future<User> getUser() async {
final supabaseUser = await _databaseClient.getUserProfile();
return supabaseUser.toUser();
}
/// Method to update user information on profiles database.
Future<void> updateUser({required User user}) {
return _databaseClient.updateUser(user: user.toSupabaseUser());
}
/// Method to do signIn.
Future<void> signIn({required String email, required bool isWeb}) async {
return _authClient.signIn(email: email, isWeb: isWeb);
}
/// Method to do signOut.
Future<void> signOut() async => _authClient.signOut();
}
extension on SupabaseUser {
User toUser() {
return User(
id: id,
userName: userName,
companyName: companyName,
);
}
}
extension on User {
SupabaseUser toSupabaseUser() {
return SupabaseUser(
id: id,
userName: userName,
companyName: companyName,
);
}
}
We're finished configuring the packages. Now we can jump into the business logic and views.
Note: These packages need different workflows to work with GitHub workflows. We can find these files in the .github folder in the example repository.
β
Supabase in Flutter
Native configuration
The first step is to configure Supabase in our project. First we need to add a few native configurations on Android and iOS.
Android: add these lines to the AndroidManifest.xml file.
<!-- Add this intent-filter for Deep Links -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
<data
android:scheme="io.supabase.flutterquickstart"
android:host="login-callback" />
</intent-filter>
iOS: add these lines to the Info.plist file.
<!-- Add this array for Deep Links -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>io.supabase.flutterquickstart</string>
</array>
</dict>
</array>
Prepare the Flutter project to use Supabase
Here we need to do a few things. First, we need to update the pubspec.yaml file in our project to be able to use all the packages that we just created, as well as the Supabase dependency.
As you might have noticed, to initialize Supabase, we need to provide the supabase url and the anon key, which can be found in the project settings.
Create the features
In this project, we are going to have four features:
Auth States Supabase: here we are going to add two classes that will tell us if a user is authenticated or not and if the authentication is required. These classes are also going to be used in our widgets.
App: this is going to be in charge of listening to the user status (authenticated or unauthenticated) to determine whether to navigate to the login page or account page.
Login: this feature is going to host all of the sign-in logic for this project.
Account: this feature is going to get the user information and display them into a text field. It will also update the information in the Supabase database and handle the sign-out.
class AuthRequiredState<T extends StatefulWidget>
extends SupabaseAuthRequiredState<T> {
@override
void onUnauthenticated() {
if (mounted) {
context.read<AppBloc>().add(AppUnauthenticated());
}
}
}
App
This feature is in charge of recovering the Supabase session to know if a user was authenticated in the previous session or not. With this information, this feature is going to determine where the application should navigate.
We are going to use Flow Builder to handle this navigation. Here we have the following structure:
LoginView: here we are going to have all the widgets needed to log in to our app. Also, we are going to use a AuthStateSupabase class to know if the user is currently logged in or not. In addition, we are going to use the input form package to check if the email is correct.
LoginBloc: the main aim of this class is communicating with the UserRepository to do login. Also, here we are going to handle all of the possible states of the email text field when the user changes the value.
AccountView: this class is going to extend to AuthRequiredState, which determines if the user is logged in. This is necessary because we are going to show the user account information.
AccountBloc: this class is in charge of multiple events. Here we need to use UserRepository to retrieve the user information, update the database, and sign out. Also, we have other events to handle user interaction with the userName and companyName text fields.
It's time to see everything working together! Fingers crossed π€
It's working! π
Extra: Personalize your email body content
You can personalize the body of the email that you send after sign-in. It is quite easy to do with Supabase. Go to the project settings in your Supabase console and follow the steps.
β
Summary
Supabase is a great tool if you are looking for login options beyond the typical ones like email/password, Twitter, Facebook, Google, and Apple. You can see here all the auth providers that Supabase can provide.
Also, if you prefer to manage your data in a SQL database, it is a great choice because Supabase uses a PostgreSQL database, in contrast with Firebase. You can find more info in the official documentation.
I highly recommend you check all the options that Supabase provides because there are tons of them with extensive documentation. Here are a few reference links: