Supabase is an open source Firebase alternative. Supabase makes super easy adding Authentication, Database, Storage, and more in your app.
In the previous article we learned how to setup a Flutter app with a complete Authentication system using Supabase.
In this post we are now going to add support for Supabase Database:
- Make
GET
request to fetch the information of the logged user - Make
UPDATE
request to update the information of the user in the database
This post will explain step by step how to add the required configuration for Supabase database and how to use the supabase_flutter
SDK to make any request to your database π
This feature is built on top of the previous setup of Supabase Authentication and the configuration of the Supabase database with Row Level Security. Make sure to follow the previous two articles before reading this.
We are also going to use some best practices to structure the app to make it ready to scale to millions of users π
Every method and class mentioned in this post has a link to the official API documentation for more details π
The final app is fully available Open Source on Github π
Overview of the app setup
This tutorial builds on top of the source code we implemented in the previous two articles:
- Flutter Supabase Authentication: we added authentication to our app, including sign up, sign in, and sign out
- Users sign up and Row Level Security: this part was all about setting up our database structure with a
user
table containingid
,first_name
, andlast_name
for all the registered users. Furthermore, we secured the access to the database by adding Row Level Security
What we are going to build next
In this article we are going to use the current setup to achieve the following:
- Read and display the information of the logged user: Based on the authenticated user
id
, we are going to perform a request to fetch from theuser
table - Update the user information: The user can write and update both his
first_name
andlast_name
directly from the app
I report here below the shape of the user
table we created in the previous article:
These requirements may look simple, but in reality they include most of the usecases for a general applications: reading and writing data from the database.
Let's jump into code π»
Add new DatabaseRepository
The first step is adding a new repository class.
A repository defines all the methods required by the app to interact with the database.
We create a new UserDatabaseRepository
, responsible to interact with the user
table in our database.
Based on our requirements, we define 2 methods:
getUserInformation
: used to get the information of a user from hisid
updateUserInformation
: used to update the userfirst_name
andlast_name
abstract class UserDatabaseRepository {
Future<UserModel> getUserInformation(String userId);
Future<UserModel> updateUserInformation(UserModel userModel);
}
Adding serializable UserModel
The UserDatabaseRepository
uses the UserModel
class. The UserModel
class maps our user
table structure inside our app.
The
UserModel
class is needed to convert the response of the API from JSON to a dart class, usable inside the application code.
We create a new models
folder. Inside it we add UserModel
:
/// Convert database model for `user` table to
/// internal dart `class`:
/// - Use `fromJson` method to convert supabase response to [UserModel]
/// - Use `toJson` method to convert [UserModel] for update request
class UserModel {
final String id;
final String? firstName;
final String? lastName;
const UserModel({
required this.id,
this.firstName,
this.lastName,
});
static UserModel fromJson(Map<String, dynamic> json) => UserModel(
id: json['id'] as String,
firstName: json['first_name'] as String?,
lastName: json['last_name'] as String?,
);
Map<String, dynamic> toJson() => <String, dynamic>{
'id': id,
'first_name': firstName,
'last_name': lastName,
};
}
As you can see, UserModel
is a simple class which defines 2 methods:
fromJson
: used to convert the JSON response from the API to aUserModel
toJson
: used to convert aUserModel
to a JSON for the update request to the API
Database structure inside SupabaseTable
class
Another utility class we are going to create is SupabaseTable
.
This abstract class defines the shape of a table in the database directly inside our app:
/// Set of all the database tables in Supabase.
///
/// Used to reference valid tables when making database requests.
abstract class SupabaseTable {
const SupabaseTable();
String get tableName;
}
We are then going to define a concrete implementation of this class for the user
table:
/// Set of all the database tables in Supabase.
///
/// Used to reference valid tables when making database requests.
abstract class SupabaseTable {
const SupabaseTable();
String get tableName;
}
class UserSupabaseTable implements SupabaseTable {
const UserSupabaseTable();
@override
String get tableName => "user";
String get idColumn => "id";
String get idFirstName => "first_name";
String get idLastName => "last_name";
}
By doing this, we avoid rewriting the name of the columns of the database manually in multiple places in the app. We are going to use this class inside our repository to safely reference columns in the database.
SupabaseDatabaseRepository
implementation
We are now ready to define a concrete implementation for the UserDatabaseRepository
:
class SupabaseDatabaseRepository implements UserDatabaseRepository {
final Supabase _supabase;
final UserSupabaseTable _userSupabaseTable;
const SupabaseDatabaseRepository(this._supabase, this._userSupabaseTable);
}
The SupabaseDatabaseRepository
takes an instance of Supabase
and UserSupabaseTable
as dependencies:
Supabase
: supabase SDK, used to make requests to the databaseUserSupabaseTable
: class we defined above, used to reference columns in theuser
table
Use supabase_flutter
SDK to make database requests
By implementing UserDatabaseRepository
we are required to define the 2 methods of the repository: getUserInformation
and updateUserInformation
.
This is where we are going to use the Supabase SDK to make requests to the database.
The code is surprisingly simple ππΌββοΈ
For getUserInformation
, we access client
from _supabase
and perform a select()
request using an eq
filter (equality) on the user id
column.
We then convert the response to a UserModel
using fromJson
, and we return it:
@override
Future<UserModel> getUserInformation(String userId) async {
final response = await _supabase.client
.from(_userSupabaseTable.tableName)
.select()
.eq(_userSupabaseTable.idColumn, userId)
.single();
final userModel = UserModel.fromJson(response as Map<String, dynamic>);
return userModel;
}
For updateUserInformation
instead we are going to use the update
method, passing the JSON of the given UserModel
by using toJson
:
@override
Future<UserModel> updateUserInformation(UserModel userModel) async {
await _supabase.client
.from(_userSupabaseTable.tableName)
.update(userModel.toJson());
return userModel;
}
This is all we need, Supabase will take care of the rest π
You can read more about all the methods available in the Supabase SDK from the official documentation
Display and Update the user information
The very final step is using these methods inside our app.
This step is more related to your Flutter app structure than with Supabase itself. I report here below the 2 widgets you can find in the example.
The UserInformationText
widget uses a FutureBuilder
to perform the getUserInformation
request and display the user information:
class UserInformationText extends StatelessWidget {
final String userId;
const UserInformationText({
required this.userId,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return FutureBuilder<UserModel>(
future: getIt<UserDatabaseRepository>().getUserInformation(userId),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
} else if (snapshot.connectionState == ConnectionState.done) {
final data = snapshot.data;
if (data != null) {
return Text(data.firstName ?? "No name");
}
return const Text("No found");
}
return const Text("Error");
},
);
}
}
The UpdateUserForm
widget instead allows the user to add a firstName
and a lastName
using TextField
. It also displays an ElevatedButton
that performs the updateUserInformation
request when clicked:
class UpdateUserForm extends StatefulWidget {
final String userId;
const UpdateUserForm({
required this.userId,
Key? key,
}) : super(key: key);
@override
State<UpdateUserForm> createState() => _UpdateUserFormState();
}
class _UpdateUserFormState extends State<UpdateUserForm> {
String firstName = "";
String lastName = "";
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
onChanged: (value) => setState(() {
firstName = value;
}),
),
TextField(
onChanged: (value) => setState(() {
lastName = value;
}),
),
ElevatedButton(
onPressed: _onClickUpdateUser,
child: const Text("Update"),
),
],
);
}
Future<void> _onClickUpdateUser() async {
try {
await getIt<UserDatabaseRepository>().updateUserInformation(
UserModel(
id: widget.userId,
firstName: firstName,
lastName: lastName,
),
);
} catch (e) {
// TODO: Show proper error to users
print("Error when updating user information");
print(e);
}
}
}
You can expand and update this widgets as you like. Supabase allows you to read and write data from the database, your Flutter app internally will then take these information and display them as you prefer π¨
That's it! If you followed all the steps, you now have a Flutter application complete with a full Authentication system, a remote database fast and secure, and a powerful SDK to make any request to the database directly from your app.
This is all you need to start building your next project π
If you are interest in learning more about flutter, dart, and Supabase you can subscribe to my newsletter here below π and follow @SandroMaglione (me π) on Twitter.
Thanks for reading.