MemFire Cloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专注于编写前端应用程序代码,加速WEB或APP应用开发。
此示例提供了使用 MemFire Cloud 和 Flutter构建简单用户管理应用程序的步骤。这包括:
- MemFire Cloud数据库:用于存储用户数据的 MemFireDB数据库。
- MemFire Cloud用户验证:用户可以使用邮箱密码登录。
- MemFire Cloud存储:用户可以上传照片。
- 行级安全策略:数据受到保护,因此个人只能访问自己的数据。
- 即时API:创建数据库表时会自动生成 API。
在本指南结束时,您将拥有一个允许用户登录和更新个人基本资料的应用程序:
创建应用
目的:我们的应用就是通过在这里创建的应用来获得数据库、云存储等一系列资源,并将获得该应用专属的API访问链接和访问密钥,用户可以轻松的与以上资源进行交互。
登录 链接: https://cloud.memfiredb.com 创建应用
创建数据表
点击应用,视图化创建数据表
- 创建profiles表;
在数据表页面,点击“新建数据表”,页面配置如下:
其中profiles表字段id和auth.users表中的uuid外键关联。
- 开启Profiles的RLS数据安全访问规则; 选中创建的Profiles表,点击表权限栏,如下图所示,点击"启用RLS"按钮
- 允许每个用户可以查看公共的个人信息资料; 点击"新规则"按钮,在弹出弹框中,选择"为所有用户启用访问权限",输入策略名称,选择"SELECT(查询)"操作,点击“创建策略”按钮,如下图。
- 仅允许用户增删改查本人的个人资料信息; 点击"新规则"按钮,在弹出弹框中,选择"根据用户ID为用户启用访问权限",输入策略名称,选择"ALL(所有)"操作,点击“创建策略”按钮,如下图。
创建avatars存储桶
创建云存储的存储桶,用来存储用户的头像图片,涉及操作包括:
- 创建一个存储桶avatars 在该应用的云存储导航栏,点击“新建Bucket”按钮,创建存储桶avatars。
- 允许每个用户可以查看存储桶avatars 选中存储桶avatars,切换到权限设置栏,点击“新规则”按钮,弹出策略编辑弹框,选择“自定义”,如下图所示:
选择SELECT操作,输入策略名称,点击“生成策略”按钮,如下图所示。
- 允许用户上传存储桶avatars; 选中存储桶avatars,切换到权限设置栏,点击“新规则”按钮,弹出策略编辑弹框,选择“自定义”,如下图所示:
选择INSERT操作,输入策略名称,点击“生成策略”按钮,如下图所示。
查看结果
所有数据表及RLS的sql(策略名称用英文代替)
-- Create a table for public "profiles"
create table profiles (
id uuid references auth.users not null,
updated_at timestamp with time zone,
username text unique,
avatar_url text,
website text,
primary key (id),
unique(username),
);
alter table profiles enable row level security;
create policy "Public profiles are viewable by everyone."
on profiles for select
using ( true );
create policy "Users can insert their own profile."
on profiles for insert
with check ( auth.uid() = id );
create policy "Users can update own profile."
on profiles for update
using ( auth.uid() = id );
-- Set up Storage!
insert into storage.buckets (id, name)
values ('avatars', 'avatars');
create policy "Avatar images are publicly accessible."
on storage.objects for select
using ( bucket_id = 'avatars' );
create policy "Anyone can upload an avatar."
on storage.objects for insert
with check ( bucket_id = 'avatars' );
获取 API密钥
现在您已经创建了一些数据库表,您可以使用自动生成的 API 插入数据。我们只需要从API设置中获取URL和anon的密钥。
在应用->概括页面,获取服务地址以及token信息。
Anon(公开)密钥是客户端API密钥。它允许“匿名访问”您的数据库,直到用户登录。登录后,密钥将切换到用户自己的登录令牌。这将为数据启用行级安全性。
注意:service_role(秘密)密钥可以绕过任何安全策略完全访问您的数据。这些密钥必须保密,并且要在服务器环境中使用,决不能在客户端或浏览器上使用。在后续示例代码中,需要提供supabaseUrl和supabaseKey。
构建应用程序
让我们从头开始构建 Flutter应用程序。
初始化一个 Flutter应用程序
我们可以flutter create用来初始化一个名为memfiredb_quickstart:
flutter pub add supabase_flutter
再运行以下命令安装依赖项。
flutter pub get
设置深层链接
现在我们已经安装了依赖项,让我们设置深度链接,以便通过魔术链接或 OAuth 登录的用户可以返回应用程序。
com.memfire.flutterquickstart://login-callback
这就是配置Memfire Cloud的结尾,其余的是平台特定的设置
对于 Android,添加一个意图过滤器以启用深链接:
android/app/src/main/AndroidManifest.xml
<manifest ...>
<!-- ... 其他的标签 -->
<application ...>
<activity ...>
<!-- ... 其他的标签 -->
<!-- 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="com.memfire.flutterquickstart"
android:host="login-callback" />
</intent-filter>
</activity>
</application>
</manifest>
对于 iOS,添加 CFBundleURLTypes 以启用深链接:
ios/Runner/Info.plist
<!-- ... 其他的标签 -->
<plist>
<dict>
<!-- ... 其他的标签 -->
<!-- Add this array for Deep Links -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.memfire.flutterquickstart</string>
</array>
</dict>
</array>
<!-- ... 其他的标签 -->
</dict>
</plist>
主要功能
我们需要创建一个可以访问我们应用程序数据的客户端,我们使用了Supabase 客户端,使用他生态里提供的功能(登录、注册、增删改查等)去进行交互。创建一个可以访问我们应用程序数据的客户端需要接口的地址(URL)和一个数据权限的令牌(ANON_KEY),我们需要去应用的概览里面去获取这两个参数然后配置到main.dart里面去。
lib/main.dart
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:memfiredb_quickstart/pages/account_page.dart';
import 'package:memfiredb_quickstart/pages/login_page.dart';
import 'package:memfiredb_quickstart/pages/splash_page.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
// TODO: Replace credentials with your own
url: 'YOUR_SUPABASE_URL',
anonKey: 'YOUR_SUPABASE_ANON_KEY',
);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Supabase Flutter',
theme: ThemeData.dark().copyWith(
primaryColor: Colors.green,
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
onPrimary: Colors.white,
primary: Colors.green,
),
),
),
initialRoute: '/',
routes: <String, WidgetBuilder>{
'/': (_) => const SplashPage(),
'/login': (_) => const LoginPage(),
'/account': (_) => const AccountPage(),
},
);
}
}
设置AuthState
为了处理 Android 和 iOS 的深链接,让我们创建一个可以做到这一点的类。创建一个AuthState类继承supabase_flutter的SupabaseAuthState类,使我们可以响应各种深链接事件。
lib/components/auth_state.dart
import 'package:flutter/material.dart';
import 'package:supabase/supabase.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:memfiredb_quickstart/utils/constants.dart';
class AuthState<T extends StatefulWidget> extends SupabaseAuthState<T> {
@override
void onUnauthenticated() {
if (mounted) {
Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
}
}
@override
void onAuthenticated(Session session) {
if (mounted) {
Navigator.of(context)
.pushNamedAndRemoveUntil('/account', (route) => false);
}
}
@override
void onPasswordRecovery(Session session) {}
@override
void onErrorAuthenticating(String message) {
context.showErrorSnackBar(message: message);
}
}
设置AuthRequiredState
我们可能希望仅在用户登录时才向用户显示某些页面。为此,我们可以创建一个AuthRequiredState类继承supabase_flutter包中的SupabaseAuthRequiredState类。在需要对用户进行身份验证的页面继承该类即可。
lib/components/auth_required_state.dart
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class AuthRequiredState<T extends StatefulWidget>
extends SupabaseAuthRequiredState<T> {
@override
void onUnauthenticated() {
/// Users will be sent back to the LoginPage if they sign out.
if (mounted) {
/// Users will be sent back to the LoginPage if they sign out.
Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
}
}
}
让我们还创建一个常量文件,以便更轻松地使用 Supbase 客户端。我们还将包含一个扩展方法声明,以showSnackBar使用一行代码调用。
lib/utils/constants.dart
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
final supabase = Supabase.instance.client;
extension ShowSnackBar on BuildContext {
void showSnackBar({
required String message,
Color backgroundColor = Colors.white,
}) {
ScaffoldMessenger.of(this).showSnackBar(SnackBar(
content: Text(message),
backgroundColor: backgroundColor,
));
}
void showErrorSnackBar({required String message}) {
showSnackBar(message: message, backgroundColor: Colors.red);
}
}
设置启动页面
让我们创建一个启动页面,在用户打开应用程序后立即显示给他们。此启动页面继承AuthState以根据用户的身份验证状态将用户重定向到适当的页面。
lib/pages/splash_page.dart
标题文本样式列表图片链接目录代码片表格注脚注释自定义列表LaTeX 数学公式插入甘特图插入UML图插入Mermaid流程图插入Flowchart流程图插入类图快捷键 代码片复制
下面展示一些 内联代码片
。
import 'package:flutter/material.dart';
import 'package:memfiredb_quickstart/components/auth_state.dart';
class SplashPage extends StatefulWidget {
const SplashPage({Key? key}) : super(key: key);
@override
_SplashPageState createState() => _SplashPageState();
}
class _SplashPageState extends AuthState<SplashPage> {
@override
void initState() {
recoverSupabaseSession();
super.initState();
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
}
设置登录页面
让我们创建一个 Flutter 组件来管理登录和注册。我们将使用 Magic Links,因此用户无需使用密码即可使用电子邮件登录。该页面也将继承AuthState,因为它将处理用户登录。
lib/pages/login_page.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:supabase/supabase.dart';
import 'package:memfiredb_quickstart/components/auth_state.dart';
import 'package:memfiredb_quickstart/utils/constants.dart';
class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key);
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends AuthState<LoginPage> {
bool _isLoading = false;
late final TextEditingController _emailController;
Future<void> _signIn() async {
setState(() {
_isLoading = true;
});
final response = await supabase.auth.signIn(
email: _emailController.text,
options: AuthOptions(
redirectTo: kIsWeb
? null
: 'com.memfire.flutterquickstart://login-callback/'));
final error = response.error;
if (error != null) {
context.showErrorSnackBar(message: error.message);
} else {
context.showSnackBar(message: 'Check your email for login link!');
_emailController.clear();
}
setState(() {
_isLoading = false;
});
}
@override
void initState() {
super.initState();
_emailController = TextEditingController();
}
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sign In')),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12),
children: [
const Text('Sign in via the magic link with your email below'),
const SizedBox(height: 18),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
),
const SizedBox(height: 18),
ElevatedButton(
onPressed: _isLoading ? null : _signIn,
child: Text(_isLoading ? 'Loading' : 'Send Magic Link'),
),
],
),
);
}
}
设置账户页面
用户登录后,我们可以允许他们编辑他们的个人资料详细信息并管理他们的帐户。让我们为此创建一个新的组件account_page.dart。请注意,此页面将继承AuthRequiredState,因为用户需要经过身份验证才能查看此页面。
lib/pages/account_page.dart
import 'package:flutter/material.dart';
import 'package:supabase/supabase.dart';
import 'package:memfiredb_quickstart/components/auth_required_state.dart';
import 'package:memfiredb_quickstart/utils/constants.dart';
class AccountPage extends StatefulWidget {
const AccountPage({Key? key}) : super(key: key);
@override
_AccountPageState createState() => _AccountPageState();
}
class _AccountPageState extends AuthRequiredState<AccountPage> {
final _usernameController = TextEditingController();
final _websiteController = TextEditingController();
String? _userId;
String? _avatarUrl;
var _loading = false;
/// Called once a user id is received within `onAuthenticated()`
Future<void> _getProfile(String userId) async {
setState(() {
_loading = true;
});
final response = await supabase
.from('profiles')
.select()
.eq('id', userId)
.single()
.execute();
final error = response.error;
if (error != null && response.status != 406) {
context.showErrorSnackBar(message: error.message);
}
final data = response.data;
if (data != null) {
_usernameController.text = (data['username'] ?? '') as String;
_websiteController.text = (data['website'] ?? '') as String;
_avatarUrl = (data['avatar_url'] ?? '') as String;
}
setState(() {
_loading = false;
});
}
/// Called when user taps `Update` button
Future<void> _updateProfile() async {
setState(() {
_loading = true;
});
final userName = _usernameController.text;
final website = _websiteController.text;
final user = supabase.auth.currentUser;
final updates = {
'id': user!.id,
'username': userName,
'website': website,
'updated_at': DateTime.now().toIso8601String(),
};
final response = await supabase.from('profiles').upsert(updates).execute();
final error = response.error;
if (error != null) {
context.showErrorSnackBar(message: error.message);
} else {
context.showSnackBar(message: 'Successfully updated profile!');
}
setState(() {
_loading = false;
});
}
Future<void> _signOut() async {
final response = await supabase.auth.signOut();
final error = response.error;
if (error != null) {
context.showErrorSnackBar(message: error.message);
}
Navigator.of(context).pushReplacementNamed('/login');
}
/// Called when image has been uploaded to Supabase storage from within Avatar widget
Future<void> _onUpload(String imageUrl) async {
final response = await supabase.from('profiles').upsert({
'id': _userId,
'avatar_url': imageUrl,
}).execute();
final error = response.error;
if (error != null) {
context.showErrorSnackBar(message: error.message);
}
setState(() {
_avatarUrl = imageUrl;
});
context.showSnackBar(message: 'Updated your profile image!');
}
@override
void onAuthenticated(Session session) {
final user = session.user;
if (user != null) {
_userId = user.id;
_getProfile(user.id);
}
}
@override
void onUnauthenticated() {
Navigator.of(context).pushReplacementNamed('/login');
}
@override
void dispose() {
_usernameController.dispose();
_websiteController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12),
children: [
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(labelText: 'User Name'),
),
const SizedBox(height: 18),
TextFormField(
controller: _websiteController,
decoration: const InputDecoration(labelText: 'Website'),
),
const SizedBox(height: 18),
ElevatedButton(
onPressed: _updateProfile,
child: Text(_loading ? 'Saving...' : 'Update')),
const SizedBox(height: 18),
ElevatedButton(onPressed: _signOut, child: const Text('Sign Out')),
],
),
);
}
}
完成后,所有环境配置好后,按照如下步骤打开浏览器运行
实现:上传头像及更新用户信息
每个 MemFire Cloud项目都配置了存储,用于管理照片和视频等大文件。
将图片上传功能添加到帐户页面
我们将使用image_picker插件从设备中选择图像。
运行以下命令进行安装。
flutter pub add image_picker
根据平台的不同,使用image_picker需要一些额外的准备。按照 README.md 上image_picker有关如何为您使用的平台进行设置的说明进行操作。
完成上述所有操作后,就该深入研究编码了。
创建上传小组件
lib/components/avatar.dart
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:memfiredb_quickstart/utils/constants.dart';
class Avatar extends StatefulWidget {
const Avatar({
Key? key,
required this.imageUrl,
required this.onUpload,
}) : super(key: key);
final String? imageUrl;
final void Function(String) onUpload;
@override
_AvatarState createState() => _AvatarState();
}
class _AvatarState extends State<Avatar> {
bool _isLoading = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
if (widget.imageUrl == null)
Container(
width: 150,
height: 150,
color: Colors.grey,
child: const Center(
child: Text('No Image'),
),
)
else
Image.network(
widget.imageUrl!,
width: 150,
height: 150,
fit: BoxFit.cover,
),
ElevatedButton(
onPressed: _isLoading ? null : _upload,
child: const Text('Upload'),
),
],
);
}
Future<void> _upload() async {
final _picker = ImagePicker();
final imageFile = await _picker.pickImage(
source: ImageSource.gallery,
maxWidth: 300,
maxHeight: 300,
);
if (imageFile == null) {
return;
}
setState(() => _isLoading = true);
final bytes = await imageFile.readAsBytes();
final fileName = '${DateTime.now().millisecondsSinceEpoch}-${imageFile.name}';
final filePath = fileName;
final response =
await supabase.storage.from('avatars').uploadBinary(filePath, bytes);
setState(() => _isLoading = false);
final error = response.error;
if (error != null) {
context.showErrorSnackBar(message: error.message);
return;
}
final imageUrlResponse =
supabase.storage.from('avatars').getPublicUrl(filePath);
widget.onUpload(imageUrlResponse.data!);
}
}
添加新的组件
然后我们可以将组件和逻辑添加到帐户页面,用于在用户上传新头像时更新avatar_url。
lib/pages/account_page.dart
import 'package:flutter/material.dart';
import 'package:supabase/supabase.dart';
import 'package:memfiredb_quickstart/components/auth_required_state.dart';
import 'package:memfiredb_quickstart/components/avatar.dart';
import 'package:memfiredb_quickstart/utils/constants.dart';
class AccountPage extends StatefulWidget {
const AccountPage({Key? key}) : super(key: key);
@override
_AccountPageState createState() => _AccountPageState();
}
class _AccountPageState extends AuthRequiredState<AccountPage> {
final _usernameController = TextEditingController();
final _websiteController = TextEditingController();
String? _userId;
String? _avatarUrl;
var _loading = false;
/// Called once a user id is received within `onAuthenticated()`
Future<void> _getProfile(String userId) async {
setState(() {
_loading = true;
});
final response = await supabase
.from('profiles')
.select()
.eq('id', userId)
.single()
.execute();
final error = response.error;
if (error != null && response.status != 406) {
context.showErrorSnackBar(message: error.message);
}
final data = response.data;
if (data != null) {
_usernameController.text = (data['username'] ?? '') as String;
_websiteController.text = (data['website'] ?? '') as String;
_avatarUrl = (data['avatar_url'] ?? '') as String;
}
setState(() {
_loading = false;
});
}
/// Called when user taps `Update` button
Future<void> _updateProfile() async {
setState(() {
_loading = true;
});
final userName = _usernameController.text;
final website = _websiteController.text;
final user = supabase.auth.currentUser;
final updates = {
'id': user!.id,
'username': userName,
'website': website,
'updated_at': DateTime.now().toIso8601String(),
};
final response = await supabase.from('profiles').upsert(updates).execute();
final error = response.error;
if (error != null) {
context.showErrorSnackBar(message: error.message);
} else {
context.showSnackBar(message: 'Successfully updated profile!');
}
setState(() {
_loading = false;
});
}
Future<void> _signOut() async {
final response = await supabase.auth.signOut();
final error = response.error;
if (error != null) {
context.showErrorSnackBar(message: error.message);
}
Navigator.of(context).pushReplacementNamed('/login');
}
/// Called when image has been uploaded to Supabase storage from within Avatar widget
Future<void> _onUpload(String imageUrl) async {
final response = await supabase.from('profiles').upsert({
'id': _userId,
'avatar_url': imageUrl,
}).execute();
final error = response.error;
if (error != null) {
context.showErrorSnackBar(message: error.message);
}
setState(() {
_avatarUrl = imageUrl;
});
context.showSnackBar(message: 'Updated your profile image!');
}
@override
void onAuthenticated(Session session) {
final user = session.user;
if (user != null) {
_userId = user.id;
_getProfile(user.id);
}
}
@override
void onUnauthenticated() {
Navigator.of(context).pushReplacementNamed('/login');
}
@override
void dispose() {
_usernameController.dispose();
_websiteController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12),
children: [
Avatar(
imageUrl: _avatarUrl,
onUpload: _onUpload,
),
const SizedBox(height: 18),
TextFormField(
controller: _usernameController,
decoration: const InputDecoration(labelText: 'User Name'),
),
const SizedBox(height: 18),
TextFormField(
controller: _websiteController,
decoration: const InputDecoration(labelText: 'Website'),
),
const SizedBox(height: 18),
ElevatedButton(
onPressed: _updateProfile,
child: Text(_loading ? 'Saving...' : 'Update')),
const SizedBox(height: 18),
ElevatedButton(onPressed: _signOut, child: const Text('Sign Out')),
],
),
);
}
}
恭喜!您现在已经使用 Flutter 和 Memefire Cloud 构建了一个功能齐全的用户管理应用程序!