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(策略名称用英文代替)
1-- Create a table for public "profiles" 2create table profiles ( 3 id uuid references auth.users not null, 4 updated_at timestamp with time zone, 5 username text unique, 6 avatar_url text, 7 website text, 8 9 primary key (id), 10 unique(username), 11); 12 13alter table profiles enable row level security; 14 15create policy "Public profiles are viewable by everyone." 16 on profiles for select 17 using ( true ); 18 19create policy "Users can insert their own profile." 20 on profiles for insert 21 with check ( auth.uid() = id ); 22 23create policy "Users can update own profile." 24 on profiles for update 25 using ( auth.uid() = id ); 26-- Set up Storage! 27insert into storage.buckets (id, name) 28values ('avatars', 'avatars'); 29 30create policy "Avatar images are publicly accessible." 31 on storage.objects for select 32 using ( bucket_id = 'avatars' ); 33 34create policy "Anyone can upload an avatar." 35 on storage.objects for insert 36 with check ( bucket_id = 'avatars' );
获取 API密钥
现在您已经创建了一些数据库表,您可以使用自动生成的 API 插入数据。我们只需要从API设置中获取URL和anon的密钥。
在应用->概括页面,获取服务地址以及token信息。

Anon(公开)密钥是客户端API密钥。它允许“匿名访问”您的数据库,直到用户登录。登录后,密钥将切换到用户自己的登录令牌。这将为数据启用行级安全性。
注意:service_role(秘密)密钥可以绕过任何安全策略完全访问您的数据。这些密钥必须保密,并且要在服务器环境中使用,决不能在客户端或浏览器上使用。在后续示例代码中,需要提供supabaseUrl和supabaseKey。
构建应用程序
让我们从头开始构建 Flutter应用程序。
初始化一个 Flutter应用程序
我们可以flutter create用来初始化一个名为memfiredb_quickstart:
1flutter pub add supabase_flutter
再运行以下命令安装依赖项。
1flutter pub get
设置深层链接
现在我们已经安装了依赖项,让我们设置深度链接,以便通过魔术链接或 OAuth 登录的用户可以返回应用程序。

1com.memfire.flutterquickstart://login-callback
这就是配置Memfire Cloud的结尾,其余的是平台特定的设置
对于 Android,添加一个意图过滤器以启用深链接:
android/app/src/main/AndroidManifest.xml
1<manifest ...> 2 <!-- ... 其他的标签 --> 3 <application ...> 4 <activity ...> 5 <!-- ... 其他的标签 --> 6 7 <!-- Add this intent-filter for Deep Links --> 8<intent-filter> 9 <action android:name="android.intent.action.VIEW" /> 10 <category android:name="android.intent.category.DEFAULT" /> 11 <category android:name="android.intent.category.BROWSABLE" /> 12 <!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST --> 13 <data 14 android:scheme="com.memfire.flutterquickstart" 15 android:host="login-callback" /> 16 </intent-filter> 17 18 </activity> 19 </application> 20</manifest>
对于 iOS,添加 CFBundleURLTypes 以启用深链接:
ios/Runner/Info.plist
1<!-- ... 其他的标签 --> 2<plist> 3<dict> 4 <!-- ... 其他的标签 --> 5 6 <!-- Add this array for Deep Links --> 7 <key>CFBundleURLTypes</key> 8 <array> 9 <dict> 10 <key>CFBundleTypeRole</key> 11 <string>Editor</string> 12 <key>CFBundleURLSchemes</key> 13 <array> 14 <string>com.memfire.flutterquickstart</string> 15 </array> 16 </dict> 17 </array> 18 <!-- ... 其他的标签 --> 19</dict> 20</plist>
主要功能
我们需要创建一个可以访问我们应用程序数据的客户端,我们使用了Supabase 客户端,使用他生态里提供的功能(登录、注册、增删改查等)去进行交互。创建一个可以访问我们应用程序数据的客户端需要接口的地址(URL)和一个数据权限的令牌(ANON_KEY),我们需要去应用的概览里面去获取这两个参数然后配置到main.dart里面去。
lib/main.dart
1import 'package:flutter/material.dart'; 2import 'package:supabase_flutter/supabase_flutter.dart'; 3import 'package:memfiredb_quickstart/pages/account_page.dart'; 4import 'package:memfiredb_quickstart/pages/login_page.dart'; 5import 'package:memfiredb_quickstart/pages/splash_page.dart'; 6 7Future<void> main() async { 8 WidgetsFlutterBinding.ensureInitialized(); 9 10 await Supabase.initialize( 11 // TODO: Replace credentials with your own 12 url: 'YOUR_SUPABASE_URL', 13 anonKey: 'YOUR_SUPABASE_ANON_KEY', 14 ); 15 runApp(MyApp()); 16} 17 18class MyApp extends StatelessWidget { 19 @override 20 Widget build(BuildContext context) { 21 return MaterialApp( 22 title: 'Supabase Flutter', 23 theme: ThemeData.dark().copyWith( 24 primaryColor: Colors.green, 25 elevatedButtonTheme: ElevatedButtonThemeData( 26 style: ElevatedButton.styleFrom( 27 onPrimary: Colors.white, 28 primary: Colors.green, 29 ), 30 ), 31 ), 32 initialRoute: '/', 33 routes: <String, WidgetBuilder>{ 34 '/': (_) => const SplashPage(), 35 '/login': (_) => const LoginPage(), 36 '/account': (_) => const AccountPage(), 37 }, 38 ); 39 } 40}
设置AuthState
为了处理 Android 和 iOS 的深链接,让我们创建一个可以做到这一点的类。创建一个AuthState类继承supabase_flutter的SupabaseAuthState类,使我们可以响应各种深链接事件。
lib/components/auth_state.dart
1import 'package:flutter/material.dart'; 2import 'package:supabase/supabase.dart'; 3import 'package:supabase_flutter/supabase_flutter.dart'; 4import 'package:memfiredb_quickstart/utils/constants.dart'; 5 6class AuthState<T extends StatefulWidget> extends SupabaseAuthState<T> { 7 @override 8 void onUnauthenticated() { 9 if (mounted) { 10 Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false); 11 } 12 } 13 14 @override 15 void onAuthenticated(Session session) { 16 if (mounted) { 17 Navigator.of(context) 18 .pushNamedAndRemoveUntil('/account', (route) => false); 19 } 20 } 21 22 @override 23 void onPasswordRecovery(Session session) {} 24 25 @override 26 void onErrorAuthenticating(String message) { 27 context.showErrorSnackBar(message: message); 28 } 29}
设置AuthRequiredState
我们可能希望仅在用户登录时才向用户显示某些页面。为此,我们可以创建一个AuthRequiredState类继承supabase_flutter包中的SupabaseAuthRequiredState类。在需要对用户进行身份验证的页面继承该类即可。
lib/components/auth_required_state.dart
1import 'package:flutter/material.dart'; 2import 'package:supabase_flutter/supabase_flutter.dart'; 3 4class AuthRequiredState<T extends StatefulWidget> 5 extends SupabaseAuthRequiredState<T> { 6 @override 7 void onUnauthenticated() { 8 /// Users will be sent back to the LoginPage if they sign out. 9 if (mounted) { 10 /// Users will be sent back to the LoginPage if they sign out. 11 Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false); 12 } 13 } 14}
让我们还创建一个常量文件,以便更轻松地使用 Supbase 客户端。我们还将包含一个扩展方法声明,以showSnackBar使用一行代码调用。
lib/utils/constants.dart
1import 'package:flutter/material.dart'; 2import 'package:supabase_flutter/supabase_flutter.dart'; 3 4final supabase = Supabase.instance.client; 5 6extension ShowSnackBar on BuildContext { 7 void showSnackBar({ 8 required String message, 9 Color backgroundColor = Colors.white, 10 }) { 11 ScaffoldMessenger.of(this).showSnackBar(SnackBar( 12 content: Text(message), 13 backgroundColor: backgroundColor, 14 )); 15 } 16 17 void showErrorSnackBar({required String message}) { 18 showSnackBar(message: message, backgroundColor: Colors.red); 19 } 20}
设置启动页面
让我们创建一个启动页面,在用户打开应用程序后立即显示给他们。此启动页面继承AuthState以根据用户的身份验证状态将用户重定向到适当的页面。
lib/pages/splash_page.dart
标题文本样式列表图片链接目录代码片表格注脚注释自定义列表LaTeX 数学公式插入甘特图插入UML图插入Mermaid流程图插入Flowchart流程图插入类图快捷键 代码片复制
下面展示一些 内联代码片。
1import 'package:flutter/material.dart'; 2import 'package:memfiredb_quickstart/components/auth_state.dart'; 3 4class SplashPage extends StatefulWidget { 5 const SplashPage({Key? key}) : super(key: key); 6 7 @override 8 _SplashPageState createState() => _SplashPageState(); 9} 10 11class _SplashPageState extends AuthState<SplashPage> { 12 @override 13 void initState() { 14 recoverSupabaseSession(); 15 super.initState(); 16 } 17 18 @override 19 Widget build(BuildContext context) { 20 return const Scaffold( 21 body: Center(child: CircularProgressIndicator()), 22 ); 23 } 24}
设置登录页面
让我们创建一个 Flutter 组件来管理登录和注册。我们将使用 Magic Links,因此用户无需使用密码即可使用电子邮件登录。该页面也将继承AuthState,因为它将处理用户登录。
lib/pages/login_page.dart
1import 'package:flutter/foundation.dart'; 2import 'package:flutter/material.dart'; 3import 'package:supabase/supabase.dart'; 4import 'package:memfiredb_quickstart/components/auth_state.dart'; 5import 'package:memfiredb_quickstart/utils/constants.dart'; 6 7class LoginPage extends StatefulWidget { 8 const LoginPage({Key? key}) : super(key: key); 9 10 @override 11 _LoginPageState createState() => _LoginPageState(); 12} 13 14class _LoginPageState extends AuthState<LoginPage> { 15 bool _isLoading = false; 16 late final TextEditingController _emailController; 17 18 Future<void> _signIn() async { 19 setState(() { 20 _isLoading = true; 21 }); 22 final response = await supabase.auth.signIn( 23 email: _emailController.text, 24 options: AuthOptions( 25 redirectTo: kIsWeb 26 ? null 27 : 'com.memfire.flutterquickstart://login-callback/')); 28 final error = response.error; 29 if (error != null) { 30 context.showErrorSnackBar(message: error.message); 31 } else { 32 context.showSnackBar(message: 'Check your email for login link!'); 33 _emailController.clear(); 34 } 35 36 setState(() { 37 _isLoading = false; 38 }); 39 } 40 41 @override 42 void initState() { 43 super.initState(); 44 _emailController = TextEditingController(); 45 } 46 47 @override 48 void dispose() { 49 _emailController.dispose(); 50 super.dispose(); 51 } 52 53 @override 54 Widget build(BuildContext context) { 55 return Scaffold( 56 appBar: AppBar(title: const Text('Sign In')), 57 body: ListView( 58 padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12), 59 children: [ 60 const Text('Sign in via the magic link with your email below'), 61 const SizedBox(height: 18), 62 TextFormField( 63 controller: _emailController, 64 decoration: const InputDecoration(labelText: 'Email'), 65 ), 66 const SizedBox(height: 18), 67 ElevatedButton( 68 onPressed: _isLoading ? null : _signIn, 69 child: Text(_isLoading ? 'Loading' : 'Send Magic Link'), 70 ), 71 ], 72 ), 73 ); 74 } 75} 76
设置账户页面
用户登录后,我们可以允许他们编辑他们的个人资料详细信息并管理他们的帐户。让我们为此创建一个新的组件account_page.dart。请注意,此页面将继承AuthRequiredState,因为用户需要经过身份验证才能查看此页面。
lib/pages/account_page.dart
1import 'package:flutter/material.dart'; 2import 'package:supabase/supabase.dart'; 3import 'package:memfiredb_quickstart/components/auth_required_state.dart'; 4import 'package:memfiredb_quickstart/utils/constants.dart'; 5 6class AccountPage extends StatefulWidget { 7 const AccountPage({Key? key}) : super(key: key); 8 9 @override 10 _AccountPageState createState() => _AccountPageState(); 11} 12 13class _AccountPageState extends AuthRequiredState<AccountPage> { 14 final _usernameController = TextEditingController(); 15 final _websiteController = TextEditingController(); 16 String? _userId; 17 String? _avatarUrl; 18 var _loading = false; 19 20 /// Called once a user id is received within `onAuthenticated()` 21 Future<void> _getProfile(String userId) async { 22 setState(() { 23 _loading = true; 24 }); 25 final response = await supabase 26 .from('profiles') 27 .select() 28 .eq('id', userId) 29 .single() 30 .execute(); 31 final error = response.error; 32 if (error != null && response.status != 406) { 33 context.showErrorSnackBar(message: error.message); 34 } 35 final data = response.data; 36 if (data != null) { 37 _usernameController.text = (data['username'] ?? '') as String; 38 _websiteController.text = (data['website'] ?? '') as String; 39 _avatarUrl = (data['avatar_url'] ?? '') as String; 40 } 41 setState(() { 42 _loading = false; 43 }); 44 } 45 46 /// Called when user taps `Update` button 47 Future<void> _updateProfile() async { 48 setState(() { 49 _loading = true; 50 }); 51 final userName = _usernameController.text; 52 final website = _websiteController.text; 53 final user = supabase.auth.currentUser; 54 final updates = { 55 'id': user!.id, 56 'username': userName, 57 'website': website, 58 'updated_at': DateTime.now().toIso8601String(), 59 }; 60 final response = await supabase.from('profiles').upsert(updates).execute(); 61 final error = response.error; 62 if (error != null) { 63 context.showErrorSnackBar(message: error.message); 64 } else { 65 context.showSnackBar(message: 'Successfully updated profile!'); 66 } 67 setState(() { 68 _loading = false; 69 }); 70 } 71 72 Future<void> _signOut() async { 73 final response = await supabase.auth.signOut(); 74 final error = response.error; 75 if (error != null) { 76 context.showErrorSnackBar(message: error.message); 77 } 78 Navigator.of(context).pushReplacementNamed('/login'); 79 } 80 81 /// Called when image has been uploaded to Supabase storage from within Avatar widget 82 Future<void> _onUpload(String imageUrl) async { 83 final response = await supabase.from('profiles').upsert({ 84 'id': _userId, 85 'avatar_url': imageUrl, 86 }).execute(); 87 final error = response.error; 88 if (error != null) { 89 context.showErrorSnackBar(message: error.message); 90 } 91 setState(() { 92 _avatarUrl = imageUrl; 93 }); 94 context.showSnackBar(message: 'Updated your profile image!'); 95 } 96 97 @override 98 void onAuthenticated(Session session) { 99 final user = session.user; 100 if (user != null) { 101 _userId = user.id; 102 _getProfile(user.id); 103 } 104 } 105 106 @override 107 void onUnauthenticated() { 108 Navigator.of(context).pushReplacementNamed('/login'); 109 } 110 111 @override 112 void dispose() { 113 _usernameController.dispose(); 114 _websiteController.dispose(); 115 super.dispose(); 116 } 117 118 @override 119 Widget build(BuildContext context) { 120 return Scaffold( 121 appBar: AppBar(title: const Text('Profile')), 122 body: ListView( 123 padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12), 124 children: [ 125 TextFormField( 126 controller: _usernameController, 127 decoration: const InputDecoration(labelText: 'User Name'), 128 ), 129 const SizedBox(height: 18), 130 TextFormField( 131 controller: _websiteController, 132 decoration: const InputDecoration(labelText: 'Website'), 133 ), 134 const SizedBox(height: 18), 135 ElevatedButton( 136 onPressed: _updateProfile, 137 child: Text(_loading ? 'Saving...' : 'Update')), 138 const SizedBox(height: 18), 139 ElevatedButton(onPressed: _signOut, child: const Text('Sign Out')), 140 ], 141 ), 142 ); 143 } 144}
完成后,所有环境配置好后,按照如下步骤打开浏览器运行


实现:上传头像及更新用户信息
每个 MemFire Cloud项目都配置了存储,用于管理照片和视频等大文件。
将图片上传功能添加到帐户页面
我们将使用image_picker插件从设备中选择图像。
运行以下命令进行安装。
1flutter pub add image_picker
根据平台的不同,使用image_picker需要一些额外的准备。按照 README.md 上image_picker有关如何为您使用的平台进行设置的说明进行操作。
完成上述所有操作后,就该深入研究编码了。
创建上传小组件
lib/components/avatar.dart
1import 'package:flutter/material.dart'; 2import 'package:image_picker/image_picker.dart'; 3import 'package:memfiredb_quickstart/utils/constants.dart'; 4 5class Avatar extends StatefulWidget { 6 const Avatar({ 7 Key? key, 8 required this.imageUrl, 9 required this.onUpload, 10 }) : super(key: key); 11 12 final String? imageUrl; 13 final void Function(String) onUpload; 14 15 @override 16 _AvatarState createState() => _AvatarState(); 17} 18 19class _AvatarState extends State<Avatar> { 20 bool _isLoading = false; 21 22 @override 23 Widget build(BuildContext context) { 24 return Column( 25 children: [ 26 if (widget.imageUrl == null) 27 Container( 28 width: 150, 29 height: 150, 30 color: Colors.grey, 31 child: const Center( 32 child: Text('No Image'), 33 ), 34 ) 35 else 36 Image.network( 37 widget.imageUrl!, 38 width: 150, 39 height: 150, 40 fit: BoxFit.cover, 41 ), 42 ElevatedButton( 43 onPressed: _isLoading ? null : _upload, 44 child: const Text('Upload'), 45 ), 46 ], 47 ); 48 } 49 50 Future<void> _upload() async { 51 final _picker = ImagePicker(); 52 final imageFile = await _picker.pickImage( 53 source: ImageSource.gallery, 54 maxWidth: 300, 55 maxHeight: 300, 56 ); 57 if (imageFile == null) { 58 return; 59 } 60 setState(() => _isLoading = true); 61 final bytes = await imageFile.readAsBytes(); 62 final fileName = '${DateTime.now().millisecondsSinceEpoch}-${imageFile.name}'; 63 final filePath = fileName; 64 final response = 65 await supabase.storage.from('avatars').uploadBinary(filePath, bytes); 66 67 setState(() => _isLoading = false); 68 69 final error = response.error; 70 if (error != null) { 71 context.showErrorSnackBar(message: error.message); 72 return; 73 } 74 final imageUrlResponse = 75 supabase.storage.from('avatars').getPublicUrl(filePath); 76 widget.onUpload(imageUrlResponse.data!); 77 } 78} 79
添加新的组件
然后我们可以将组件和逻辑添加到帐户页面,用于在用户上传新头像时更新avatar_url。
lib/pages/account_page.dart
1import 'package:flutter/material.dart'; 2import 'package:supabase/supabase.dart'; 3import 'package:memfiredb_quickstart/components/auth_required_state.dart'; 4import 'package:memfiredb_quickstart/components/avatar.dart'; 5import 'package:memfiredb_quickstart/utils/constants.dart'; 6 7class AccountPage extends StatefulWidget { 8 const AccountPage({Key? key}) : super(key: key); 9 10 @override 11 _AccountPageState createState() => _AccountPageState(); 12} 13 14class _AccountPageState extends AuthRequiredState<AccountPage> { 15 final _usernameController = TextEditingController(); 16 final _websiteController = TextEditingController(); 17 String? _userId; 18 String? _avatarUrl; 19 var _loading = false; 20 21 /// Called once a user id is received within `onAuthenticated()` 22 Future<void> _getProfile(String userId) async { 23 setState(() { 24 _loading = true; 25 }); 26 final response = await supabase 27 .from('profiles') 28 .select() 29 .eq('id', userId) 30 .single() 31 .execute(); 32 final error = response.error; 33 if (error != null && response.status != 406) { 34 context.showErrorSnackBar(message: error.message); 35 } 36 final data = response.data; 37 if (data != null) { 38 _usernameController.text = (data['username'] ?? '') as String; 39 _websiteController.text = (data['website'] ?? '') as String; 40 _avatarUrl = (data['avatar_url'] ?? '') as String; 41 } 42 setState(() { 43 _loading = false; 44 }); 45 } 46 47 /// Called when user taps `Update` button 48 Future<void> _updateProfile() async { 49 setState(() { 50 _loading = true; 51 }); 52 final userName = _usernameController.text; 53 final website = _websiteController.text; 54 final user = supabase.auth.currentUser; 55 final updates = { 56 'id': user!.id, 57 'username': userName, 58 'website': website, 59 'updated_at': DateTime.now().toIso8601String(), 60 }; 61 final response = await supabase.from('profiles').upsert(updates).execute(); 62 final error = response.error; 63 if (error != null) { 64 context.showErrorSnackBar(message: error.message); 65 } else { 66 context.showSnackBar(message: 'Successfully updated profile!'); 67 } 68 setState(() { 69 _loading = false; 70 }); 71 } 72 73 Future<void> _signOut() async { 74 final response = await supabase.auth.signOut(); 75 final error = response.error; 76 if (error != null) { 77 context.showErrorSnackBar(message: error.message); 78 } 79 Navigator.of(context).pushReplacementNamed('/login'); 80 } 81 82 /// Called when image has been uploaded to Supabase storage from within Avatar widget 83 Future<void> _onUpload(String imageUrl) async { 84 final response = await supabase.from('profiles').upsert({ 85 'id': _userId, 86 'avatar_url': imageUrl, 87 }).execute(); 88 final error = response.error; 89 if (error != null) { 90 context.showErrorSnackBar(message: error.message); 91 } 92 setState(() { 93 _avatarUrl = imageUrl; 94 }); 95 context.showSnackBar(message: 'Updated your profile image!'); 96 } 97 98 @override 99 void onAuthenticated(Session session) { 100 final user = session.user; 101 if (user != null) { 102 _userId = user.id; 103 _getProfile(user.id); 104 } 105 } 106 107 @override 108 void onUnauthenticated() { 109 Navigator.of(context).pushReplacementNamed('/login'); 110 } 111 112 @override 113 void dispose() { 114 _usernameController.dispose(); 115 _websiteController.dispose(); 116 super.dispose(); 117 } 118 119 @override 120 Widget build(BuildContext context) { 121 return Scaffold( 122 appBar: AppBar(title: const Text('Profile')), 123 body: ListView( 124 padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 12), 125 children: [ 126 Avatar( 127 imageUrl: _avatarUrl, 128 onUpload: _onUpload, 129 ), 130 const SizedBox(height: 18), 131 TextFormField( 132 controller: _usernameController, 133 decoration: const InputDecoration(labelText: 'User Name'), 134 ), 135 const SizedBox(height: 18), 136 TextFormField( 137 controller: _websiteController, 138 decoration: const InputDecoration(labelText: 'Website'), 139 ), 140 const SizedBox(height: 18), 141 ElevatedButton( 142 onPressed: _updateProfile, 143 child: Text(_loading ? 'Saving...' : 'Update')), 144 const SizedBox(height: 18), 145 ElevatedButton(onPressed: _signOut, child: const Text('Sign Out')), 146 ], 147 ), 148 ); 149 } 150}
恭喜!您现在已经使用 Flutter 和 Memefire Cloud 构建了一个功能齐全的用户管理应用程序!
