使用 Supabase 在 Flutter 中构建简单的杂货店应用程序
介绍
演示
数据库设计
关系
Supabase 设置
创建表
Flutter 数据模型
Flutter 设置
Supabase 查询
验证
Supabase 服务
概括
介绍
在本文中,我将向您展示如何使用 Flutter 构建一个以 Supabase 为后端的应用程序。Supabase 是 Firebase 的替代品,它使用 Postgres,这很独特,甚至令人惊叹。Postgres 也是一个关系数据库,同样是开源的,并且功能强大。
我们将学习并使用 Flutter 和 Supabase 构建一个简单的杂货应用程序。
我不会一步一步地介绍如何设置 Flutter,因为我的构建方式使用了遵循 MVVM 风格的 Stacked 架构,所以我只会向您展示如何在 Flutter 应用程序中用 Dart 编写 Supabase 代码。
您可以在FilledStacks上了解有关 Stacked 架构的更多信息:D
我的 SupaGrocery 仓库将在本教程的最后分享。你可以直接下载。
演示
数据库设计
在开始其他操作之前,我们先来看看数据库设计。见下方附图。
对于我们的简单杂货应用程序,我们只需要 4 个这样的表。
app_users
users
:这是我们存储用户的表,它将与 supabase auth 用户具有相同的主 ID。由于无法公开读取,我无法仅使用这个表,因此我必须创建此表。groceries
:每个用户的所有购物清单都将存储在此表中。products
:用户创建的所有商品都会存储在这张表中。grocery_products
:我们在这里将产品与杂货店联系起来。这就是我们所说的数据透视表。
关系
在关系数据库中,表关系是非常常见的事情,也是我在关系数据库中最喜欢的东西。
这两种是最常见的关系:
- 一对一
- 一对多
- 多对多(数据透视表)
我们的app_users
表与我们创建的两个表具有一对多关系products
,groceries
因为用户可以拥有许多购物清单,并且在该购物清单中也可以拥有许多产品。
然后,对于我们的groceries
表,我们将该created_by
列作为外键,以便链接到该app_users
表,然后将其标识为我们应用程序中用户杂货清单的一部分。
products
对于以列作为外键的表created_by
也是如此。
然后对于我们的数据透视表来说,它是一个多对多关系,因为一个杂货店清单可以有许多产品,而一个产品可以属于许多杂货店清单。
Supabase 设置
创建你的第一个 Supabase 帐户!前往他们的官方网站https://supabase.io/ 。
应该带你去这个精彩的黑暗主题网站:D
现在继续并单击该按钮“开始您的项目”
它将显示此 auth0 页面,因此只需继续使用 GitHub 即可立即注册!
然后只需使用您的 GitHub 凭据登录。
创建完第一个帐户后,您可能已经进入了仪表板,其中列出了您在 Supabase 中创建的所有项目。
现在点击“新建项目”,然后选择您想要的组织。我选择的是修改后的“个人”。
进入此页面后,只需填写以下字段:
名称:“Grocery App”
数据库密码:“s0m3Str0ng_PassWord!@#”(您应该使用自己的密码)
地区:(选择您附近的任何东西)
完成后单击“创建新项目”!
然后它会将您重定向到此页面。
这将需要几分钟,请等待:)
创建表
当 Supabase 设置完毕,并且您创建了新项目后,它将带您进入此页面。
现在让我们点击“创建新表”
我们将提供数据库设计中的所有细节,因此这个设置应该非常快。
我的建议是取消勾选“包含主键”,稍后在创建表时再添加主键。这里面有个 bug,导致我无法为该uuid
键设置默认值,只能uuid
在创建新记录时自动生成一个。
然后点击右上角的“保存”即可最终创建表格。
创建该表后,我们可以继续添加主键,即uuid
。单击该加号图标可为表添加新列。
然后将该列命名为id
,它将成为主键和类型,uuid
然后具有默认值“自动生成 UUID”,完成后单击“保存”。
完成后,我们可以继续创建更多我们从数据库设计中定义的列。
接下来,我们将创建一个表,products
并为此表设置外键,因为每个产品都属于一个用户。我们将快速学习如何操作。
因此,假设您已经创建了主键id
及其对应的name
varchar 列,让我们创建最后一个字段created_by
并将其设置为与表链接的外键app_users
。
现在点击底部的“添加外键关系”按钮
然后选择表app_users
和id
字段,完成后单击“保存”
然后应该向您显示它现在已与表格链接app_users
,这非常令人惊奇。
这就是设置外键所需了解的全部内容。现在,剩下的表就交给你了。你懂的!
Flutter 数据模型
freezed
我们将使用包来设置我们的数据模型,json_serializable
并确保builder_runner
在您的项目中有一个设置。
以下是我们的应用程序数据模型
import 'package:freezed_annotation/freezed_annotation.dart';
part 'application_models.freezed.dart';
part 'application_models.g.dart';
@freezed
class AppUser with _$AppUser {
const factory AppUser({
required String id,
required String name,
required String email,
}) = _AppUser;
factory AppUser.fromJson(Map<String, dynamic> json) =>
_$AppUserFromJson(json);
}
@freezed
class Grocery with _$Grocery {
const Grocery._();
const factory Grocery({
required String id,
required String name,
@JsonKey(name: 'created_by')
required String createdBy,
@Default([])
@JsonKey(
name: 'grocery_products',
fromJson: Grocery._productsFromJson,
toJson: Grocery._productsToJson,
)
List<GroceryProduct>? groceryProducts,
}) = _Grocery;
bool get hasGroceryProducts => groceryProducts!.length > 0;
List<Product?>? get products {
if (!hasGroceryProducts) return [];
return groceryProducts!.map((e) => e.product).toList();
}
factory Grocery.fromJson(Map<String, dynamic> json) =>
_$GroceryFromJson(json);
static List<GroceryProduct>? _productsFromJson(List<dynamic>? list) {
if (list == null) {
return [];
}
return list.map((e) => GroceryProduct.fromJson(e)).toList();
}
static List<Map<String, dynamic>>? _productsToJson(
List<GroceryProduct>? list) {
if (list == null) {
return [];
}
return list.map((e) => e.toJson()).toList();
}
}
@freezed
class GroceryDto with _$GroceryDto {
const factory GroceryDto({
required String name,
@JsonKey(name: 'created_by') required String createdBy,
}) = _GroceryDto;
factory GroceryDto.fromJson(Map<String, dynamic> json) =>
_$GroceryDtoFromJson(json);
}
@freezed
class Product with _$Product {
const factory Product({
required String id,
required String name,
@JsonKey(name: 'created_by') required String createdBy,
}) = _Product;
factory Product.fromJson(Map<String, dynamic> json) =>
_$ProductFromJson(json);
}
@freezed
class ProductDto with _$ProductDto {
const factory ProductDto({
required String name,
@JsonKey(name: 'created_by') required String createdBy,
}) = _ProductDto;
factory ProductDto.fromJson(Map<String, dynamic> json) =>
_$ProductDtoFromJson(json);
}
@freezed
class GroceryProduct with _$GroceryProduct {
const factory GroceryProduct({
required String id,
@JsonKey(name: 'grocery_id') required String groceryId,
@JsonKey(name: 'product_id') required String productId,
required int quantity,
@JsonKey(name: 'products') Product? product,
@Default('') String? unit,
}) = _GroceryProduct;
factory GroceryProduct.fromJson(Map<String, dynamic> json) =>
_$GroceryProductFromJson(json);
}
@freezed
class GroceryProductDto with _$GroceryProductDto {
const factory GroceryProductDto({
@JsonKey(name: 'grocery_id') required String groceryId,
@JsonKey(name: 'product_id') required String productId,
@Default(1) int quantity,
String? unit,
}) = _GroceryProductDto;
factory GroceryProductDto.fromJson(Map<String, dynamic> json) =>
_$GroceryProductDtoFromJson(json);
}
@freezed
class AuthDto with _$AuthDto {
const factory AuthDto({
required String email,
required String password,
String? name,
}) = _AuthDto;
factory AuthDto.fromJson(Map<String, dynamic> json) =>
_$AuthDtoFromJson(json);
}
上面的代码将生成以下文件
我们不必编写所有内容,只需让它自动生成即可build_runner
为了让您了解我们的数据模型,我们看到我们有用于杂货应用程序的主要表。
- 应用用户
- 杂货店
- 产品
- 杂货产品
DTO
- GroceryDto
- 产品数据
- 杂货产品Dto
- 授权
但是那些带有“Dto”名称的数据模型是什么?
DTO 只是数据传输对象的意思,我喜欢在我发出的任何 API 请求中使用 DTO。
数据传输对象 (DTO) 是一种用于封装数据并将其从应用程序的一个子系统发送到另一个子系统的对象。N 层应用程序中的服务层最常使用 DTO 在其自身与 UI 层之间传输数据。
Flutter 设置
安装并设置 Flutter 应用。然后使用以下依赖项来设置 Supabase。
软件包:
我添加了postgrest
,因为我想从包中获取所有类型,而 Supabase 正在使用它们。
完成后,您可以继续设置 Supabase 客户端
import 'package:supabase/supabase.dart';
// use your own SUPABASE_URL
const String SUPABASE_URL = 'https://borayzhhitkyveigfijz.supabase.co';
// use your own SUPABASE_SECRET key
const String SUPABASE_SECRET =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYxOTMwODI5MCwiZXhwIjoxOTM0ODg0MjkwfQ.Kk1ckyjzCB98aWyBPtJsoWuTsbq2wyYfiUxG7fH4yAg';
final SupabaseClient supabase = SupabaseClient(SUPABASE_URL, SUPABASE_SECRET);
这些可以在项目设置的 API 选项卡中找到。要获取 SUPABASE_URL
SUPABASE_SECRET
然后,当设置完成后,我们可以进行查询!
Supabase 查询
如果您了解 SQL 或熟悉它,那么感觉应该非常相似。
但这些将由 Supabase 自动生成,所以如果您不知道如何构建 Supabase 查询,也不用担心。只需检查项目 API,它会在您更新表或更改任何列时动态生成。
相比之下,这是一个 RAW SQL 查询。
SELECT * FROM products
这就是在 Dart 中使用 Supabase 编写查询的方法
supabase.from("products").select().execute();
确保execute
最后一部分始终有,否则它将无法从products
表中获取所有数据。
那么查询单个记录怎么样?
在 SQL 中,我们有
SELECT * FROM products WHERE id = "uuid-string";
在 Supabase Dart 中,我们有
supabase.from("products").select().eq("id", "uuid-string").single().execute();
您的 Supabase 项目中还有更多查询可供展示,因此请务必在此处查看
验证
在每个应用程序中,保护用户数据安全的一种方法就是拥有一个身份验证系统。因此,使用 Supabase,您可以非常轻松地立即开始身份验证,因为他们提供了一个非常简单直观的 API!
class AuthenticationService {
final _logger = Logger();
final _localStorageService = locator<LocalStorageService>();
AppUser? _user = null;
AppUser? get user => _user;
bool get hasUser => _user != null;
Future<void> initialize() async {}
Future<AppUser?> signIn({required AuthDto payload}) async {}
Future<AppUser?> signUp({required AuthDto payload}) async {}
Future<void> signOut() async {}
Future<AppUser?> fetchUser({required String id}) async {}
Future<PostgrestResponse> _createUser(User user, AuthDto payload) {}
}
把上面的代码分解一下。这依赖于本地存储服务(Shared Preferences),我们将在其中存储 JWT 授权令牌/刷新令牌,以及用于调试的 Logger。所以我喜欢随身携带一个 Logger。
我们有一个私有属性,我们使用自己的 getter 和布尔 getter 来存储我们的用户,如果该属性不为空,_user
则检查用户是否已登录。_user
在这个initialize()
方法内部,我们将执行自动登录。因此,如果用户的本地存储中存储了刷新令牌,我们将继续登录该用户,获取用户数据并将其存储在_user
属性中,这样hasUser
布尔值 getter 的值就会为 true。
Future<void> initialize() async {
final accessToken = await _localStorageService.getItem('token');
_logger.i(accessToken);
if (accessToken == null) {
return;
}
final response = await supabase.auth.api.getUser(accessToken);
if (response.error != null) {
return;
}
final user = response.data!;
_logger.i(user.toJson());
await fetchUser(id: user.id);
}
接下来是signIn
带有参数的方法,AuthDto
该参数包含email
一个password
字段。当用户提供正确且现有的电子邮件时,我们将获取他们的访问令牌并将其存储在本地存储中。
Future<AppUser?> signIn({required AuthDto payload}) async {
final response = await supabase.auth.signIn(
email: payload.email,
password: payload.password,
);
if (response.error != null) {
_logger.e(response.error!.message);
return null;
}
_logger.i(response.data);
await _localStorageService.setItem('token', response.data!.accessToken);
return await fetchUser(id: response.data!.user!.id);
}
signUp
每当有新用户想要使用我们的应用时,我们都会使用该方法。创建新用户时,我们会获取访问令牌并将其保存到本地存储。我们还会在app_users
表中创建新的用户记录,但会使用另一个名为_createUser
Future<AppUser?> signUp({required AuthDto payload}) async {
final response =
await supabase.auth.signUp(payload.email, payload.password);
if (response.error != null) {
_logger.e(response.error!.message);
return null;
}
final user = response.data!.user!;
_logger.i(user.toJson());
await _createUser(user, payload);
await _localStorageService.setItem('token', response.data!.accessToken);
return await fetchUser(id: user.id);
}
_createdUser
将在表中创建一个新的用户记录app_users
。
Future<PostgrestResponse> _createUser(User user, AuthDto payload) {
return supabase
.from("app_users")
.insert(
AppUser(
id: user.id,
name: payload.name!,
email: user.email,
),
)
.execute();
}
然后,signOut
这已经不言自明了。这里我们只是在用户决定删除访问令牌时,从本地存储中删除访问令牌。signOut
Future<void> signOut() async {
final response = await supabase.auth.signOut();
if (response.error != null) {
_logger.e(response.error!.message);
return;
}
_logger.i(response.rawData);
await _localStorageService.removeItem('token');
return;
}
最后fetchUser
,我们有一种方法可以获取当前已验证的用户记录,以便我们随时可以在整个应用程序中获取他们的信息。
Future<AppUser?> fetchUser({required String id}) async {
final response = await supabase
.from("app_users")
.select()
.eq('id', id)
.single()
.execute();
_logger.i(
'Count: ${response.count}, Status: ${response.status}, Data: ${response.data}',
);
if (response.error != null) {
_logger.e(response.error!.message);
return null;
}
_logger.i(response.data);
final data = AppUser.fromJson(response.data);
_user = data;
return data;
}
Supabase 服务
我们完成了数据模型和身份验证的处理,接下来就可以为应用程序创建和处理读写操作了。得益于抽象的概念,我们无需为相同的功能编写大量代码,只需编写更少的代码,即可将此功能扩展到其他需要它的服务。
以下是处理 CRUD 操作(Cread、Read、Update、Delete)的抽象类
import 'package:logger/logger.dart';
import 'package:postgrest/postgrest.dart';
import 'package:supagrocery/app/app.locator.dart';
import 'package:supagrocery/app/supabase_api.dart';
import 'package:supagrocery/services/authentication_service.dart';
abstract class SupabaseService<T> {
final _authService = locator<AuthenticationService>();
final _logger = Logger();
String tableName() {
return "";
}
Future<PostgrestResponse> all() async {
_logger.i(tableName());
final response = await supabase
.from(tableName())
.select()
.eq('created_by', _authService.user!.id)
.execute();
_logger.i(response.toJson());
return response;
}
Future<PostgrestResponse> find(String id) async {
_logger.i(tableName() + ' ' + id);
final response = await supabase
.from(tableName())
.select()
.eq('id', id)
.single()
.execute();
_logger.i(response.toJson());
return response;
}
Future<PostgrestResponse> create(Map<String, dynamic> json) async {
_logger.i(tableName() + ' ' + json.toString());
final response = await supabase.from(tableName()).insert(json).execute();
_logger.i(response.toJson());
return response;
}
Future<PostgrestResponse> update({
required String id,
required Map<String, dynamic> json,
}) async {
_logger.i(tableName() + ' ' + json.toString());
final response =
await supabase.from(tableName()).update(json).eq('id', id).execute();
_logger.i(response.toJson());
return response;
}
Future<PostgrestResponse> delete(String id) async {
_logger.i(tableName() + ' ' + id);
final response =
await supabase.from(tableName()).delete().eq('id', id).execute();
_logger.i(response.toJson());
return response;
}
}
这个抽象类依赖于AuthenticationService
我们刚刚创建的,因此每次用户在我们的数据库中创建记录时,我们都可以附加用户的 ID。
我们将为tableName
每个需要的功能服务重写 。因此,在创建ProductService
和 时,我们只需使用相应的表名GroceryService
扩展此类和该重写即可。tableName
这是一个例子ProductService
import 'package:postgrest/postgrest.dart';
import 'package:supagrocery/app/app.locator.dart';
import 'package:supagrocery/app/supabase_api.dart';
import 'package:supagrocery/datamodels/application_models.dart';
import 'package:supagrocery/services/authentication_service.dart';
import 'package:supagrocery/services/supabase_service.dart';
class ProductService extends SupabaseService<Product> {
final _authService = locator<AuthenticationService>();
@override
String tableName() {
return "products";
}
Future<PostgrestResponse> fetchProducts() async {
return await supabase
.from("products")
.select("*")
.eq('created_by', _authService.user!.id)
.execute();
}
}
这还将包含SupabaseService
我们创建的抽象类中的方法,并且无需重写任何内容,我们只需重写tableName
并返回该表的名称即可。有了它,ProductService
我们就可以编写任何与业务逻辑相关的方法了。
那么这是我们的GroceryService
import 'package:postgrest/postgrest.dart';
import 'package:supagrocery/app/app.locator.dart';
import 'package:supagrocery/app/supabase_api.dart';
import 'package:supagrocery/datamodels/application_models.dart';
import 'package:supagrocery/services/supabase_service.dart';
import 'authentication_service.dart';
class GroceryService extends SupabaseService<Grocery> {
final _authService = locator<AuthenticationService>();
@override
String tableName() {
return "groceries";
}
Future<PostgrestResponse> fetchGroceryList({required String id}) async {
return await supabase
.from("groceries")
.select(", grocery_products(, products(*) )")
.eq('id', id)
.eq('created_by', _authService.user!.id)
.single()
.execute();
}
Future<PostgrestResponse> addProductsToList({
required String id,
required List<Product?> products,
}) async {
return await supabase
.from("grocery_products")
.insert(
products.map((e) {
return GroceryProductDto(
groceryId: id,
productId: e!.id,
).toJson();
}).toList(),
)
.execute();
}
Future<PostgrestResponse> markProductChecked(
{required GroceryProduct payload}) async {
return await supabase
.from("grocery_products")
.update(payload.toJson())
.eq('id', payload.id)
.execute();
}
Future<PostgrestResponse> removeProduct({required String id}) async {
return await supabase
.from("grocery_products")
.delete()
.eq('id', id)
.execute();
}
}
概括
我们介绍了数据库设计、Supabase 设置、使用 Supabase API 实现身份验证系统以及使用抽象轻松实现新功能。
我希望这能给你一个想法并且在某种程度上是有用的。
感谢您的阅读并希望您喜欢!
鏂囩珷鏉ユ簮锛�https://dev.to/carlomigueldy/building-a-simple-grocery-app-in-flutter-with-supabase-5fad